Bicrypto 6.7.8

Pre-release

This version isn’t published on updates.mashdiv.com yet — the notes are available to preview, but it can’t be downloaded until it’s released.

11 September 2026

This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.

INSTALLFINANCETRANSFERSDEPOSITSWITHDRAWALSPERFORMANCERATE LIMITSLOGGINGADDONSI18N

Core v6.7.8

Release Date: September 11, 2026 Tags: INSTALL, FINANCE, TRANSFERS, DEPOSITS, WITHDRAWALS, PERFORMANCE, RATE LIMITS, LOGGING, ADDONS, I18N

Overview

If you do not run the Ecosystem addon, 6.7.7 could not start, and pnpm updator died before the schema migration ran. A core file imported an Ecosystem file at module scope; the require chain reaches server.ts, so the absence of an optional addon was a whole-platform boot crash rather than a missing feature. Anyone who tried to update to 6.7.7 without Ecosystem installed and saw MODULE_NOT_FOUND was hitting this. This release is the fix, and it is why there is a 6.7.8 at all.

The platform was slow while a bot ran, and the cause was the platform's own logging. A CPU profile taken while a bot placed and cancelled about ten orders a second put the backend at 91% of one core, with 13.3% of that busy time spent writing to the console and 28.5% in the licence layer. Node is single-threaded, so both come straight out of the event loop every other request is queued behind. Neither was producing anything anybody read.

The finance wizards stated as fact things they had not yet asked. An empty list has several causes — not fetched yet, genuinely empty, request failed, superseded by a newer click — and every step of deposit, transfer and withdraw collapsed them into one. Customers were told they own no wallets, that no currency can be deposited, and that there is nothing to transfer, while the request that would have said otherwise was still in flight or had failed with a banner saying so directly above.

A malformed RATE_LIMIT silently removed the per-IP cap from the entire platform, login and registration included. parseInt("unlimited", 10) is NaN, every comparison against NaN is false, and the limiter failed open.

It is all in Upgrade Notes; read it before you update.

Addons released alongside this. Work in the extension trees ships in their own notes, and several of them want this release underneath:

  • Ecosystem 6.5.2 — three ScyllaDB readers were returning their first page and reporting it as the whole answer, one of which could release the collateral backing a live order. Requires Core v6.7.8 for the rate limiter that moved here.
  • Hummingbot Connector 6.1.9 — the backend was billing its own queue delay to the bot's clock and charging it as a forged signature. The two core files behind that (handler/Request.ts, handler/Routes.ts) change in this release; the behaviour is explained there.
  • Futures 6.2.5 — a cancelled order could be revived by a read taken before it was cancelled.
  • AI Market Maker 6.2.6 — one line, and it belongs to the Ecosystem fix above.
  • P2P Trading 6.3.7 — "top cryptocurrencies" was adding up different currencies.

Update Instructions

pnpm updator

There is no schema change in this release — no new table, no seeder, and nothing to run by hand. Both halves matter: the licence cache, the logging changes and the rate-limit guard are server-side and need the backend restart; the deposit, transfer and withdraw wizards live in the browser bundle and need the frontend rebuild.

  • Update Core first, before the addons listed above. Ecosystem 6.5.2 re-exports a module that moves into Core in this release, so Core has to be in place for the addon's own path to resolve.
  • Check RATE_LIMIT and RATE_LIMIT_EXPIRE in your .env before you restart. If either is not a positive number, you have been running with no per-IP cap or with a bucket that never expired. See Upgrade Notes.
  • Per-step request logs are quieter by default. LOG_LEVEL=debug restores them in full; nothing durable changed. See Upgrade Notes.

Upgrade Notes

An install without the Ecosystem addon could not boot 6.7.7

utils/pool-backing/custody.ts imported the rate limiter from api/(ext)/ecosystem/deposit/util/RateLimiter at module scope. That file belongs to an optional addon, and the import chain above it does not stop:

utils/pool-backing/custody.ts
  <- utils/pool-backing/reconcile.ts
  <- cron/jobs/poolBacking.ts
  <- cron/index.ts
  <- server.ts

So on any install without Ecosystem the process died at boot with MODULE_NOT_FOUND, and because pnpm updator boots the backend to run the schema migration, the update itself failed before the migration ran. Core may not depend on an addon; an addon may always depend on core.

The limiter has moved to core at utils/chain-rate-limit.ts. The addon path still exists and re-exports it with the identical export surface, so an install still carrying the old file — or an older Ecosystem addon — resolves either way and nothing needs reordering. The "one set of buckets per process" guarantee now holds more firmly than before: the singleton lives in core, which is always present, so the deposit scanner and the pool-backing custody reads share one budget against the public chain endpoints whatever is installed.

This was introduced on 5 September, which is after 6.7.6 was numbered, so 6.7.7 is the released version that carries it. If your update to 6.7.7 failed, go straight to 6.7.8.

Check your rate-limit settings before restarting

RATE_LIMIT and RATE_LIMIT_EXPIRE were read with a bare parseInt. A value that does not parse yields NaN, the limiter decides with if (used >= limit), and every comparison against NaN is false — so a typo did not fall back to the default, it removed the per-IP cap from the whole platform, including the login and registration doors the limiter exists to protect. RATE_LIMIT_EXPIRE had the mirror-image fault: a NaN TTL left the counter key with no expiry, so the bucket filled once and refused for ever.

From this release anything that is not a finite positive number falls back to the default and prints one warning per variable per process naming the value it rejected. There is deliberately no value meaning "off".

What to do. Read the two variables in your .env now. If either is misspelled, empty-but-present, or set to a word, your install has been running without a cap (or with a permanently jammed one), and this release will change that on the first restart — which is the correct behaviour, but it is a change you should not meet by surprise.

Per-step request logs are quieter, and LOG_LEVEL=debug brings them back

A CPU profile taken while a bot placed and cancelled about ten orders a second put the backend process at 91% of one core with 13.3% of that busy time in console output (writeUtf8String / writeBuffer / consoleCall). Two writers accounted for it, and neither was producing anything anyone read:

  • Every HTTP request opens a live task, and placeOrder alone reports thirteen steps — a header, ~13 step lines and a closing line, fifteen synchronous writes to a pipe per request. On Linux a write to a pipe blocks when the reader is behind, and in production the reader is the pm2 daemon.
  • The wallet audit logger ran JSON.stringify on every wallet operation and wrote ~400 bytes that duplicated, verbatim, the durable row written immediately after it. A market maker performs at least two wallet operations per order.

What is kept. The request header and the closing line with its duration still print for every request, so traffic and timings stay visible. A step that warned or failed still prints, because those are the ones worth reading and they are rare. The full step breakdown of any slow request is unaffected — the SLOW line is built from the context's own step array, not from the console handle, so it still names exactly where the time went. And a developer watching a TTY sees no change at all; only the non-TTY (pm2 pipe) branch is gated.

Nothing about the durable audit trail changes. walletAuditLog is still written inside the caller's transaction, and a persistence failure still warns unconditionally. What was removed is the console copy of a row that is already stored.

LOG_LEVEL=debug restores both in full.

The currency route's empty answer changed shape

GET /api/finance/currency?action=transfer now always answers {from, to}. Four paths used to short-circuit with a bare [] for every action — the three "this wallet type is switched off" guards and the "you hold nothing funded" return — so a transfer caller reading data.from got undefined and could not tell "no currencies" from "the request went wrong". That is exactly how the transfer wizard came to render a blank step with nothing on it explaining why.

Any client reading data.from / data.to is unaffected and now gets the empty case correctly. A client that treated the transfer response as an array was already broken on the populated path, because that path has always returned an object.

The route's declared response schema was also wrong on every path and is corrected — it described a {status, data} envelope this route does not send, an array for an action that returns an object, and database columns where the rows are {value, label} picker options. Any SDK, mock or contract test generated from it was wrong; regenerate them.


Added

  • Added utils/chain-rate-limit.ts in core, holding the per-chain token buckets that every in-process reader of the public chain endpoints draws from. The Ecosystem addon's path re-exports it unchanged. See Upgrade Notes for the boot crash this resolves.
  • Added requireOptionalModuleAtLoad(module, importer), the module-scope variant of the optional-import helper. At module scope a throw is not an error a caller can handle — it aborts the importing file's own load, so one bad addon takes a core file down with it. Absence returns null silently; a real defect returns null too, but loudly, at error level, naming both the module and the importer. The old bare catch (e) {} could not tell the two apart and said nothing either way.
  • Added five finance-wizard message keys — covering "no depositable currencies here", "no destination wallet for this source", "no funded currencies to transfer out", "no target currencies that could receive this", and "no wallet types are switched on" — seeded into all 89 catalogues. no_wallets_available_description previously rendered as the literal text "No Wallets Available Description".

Changed

  • Changed the licence route lookups to be memoised. isLicenseExemptRoute and findExtensionForRoute run on every HTTP request and walk static tables — 8 exempt prefixes and 19 extensions holding 36 route prefixes between them. In source that is trivial; in a shipped build it is not, because this file is obfuscated and every literal comparison runs through the string-array decoder. The profile put the licence layer at 28.5% of busy time under bot load. Both functions are pure functions of the path over tables nothing mutates, so the answer can never change within a process. The cache is bounded at 5,000 entries and cleared wholesale at the cap, because a path like /api/hb/order/<uuid> is one path per order and an unbounded map would be a memory leak with a public trigger. No security property changes: nothing here decides whether a licence is valid.
  • Changed handler/Request.ts and handler/Routes.ts to stamp each request's arrival time and to allow a signed bot route's body-schema check to be held until after its signature verifies. Both exist to serve the Hummingbot bridge and neither alters any other route; Hummingbot Connector 6.1.9 explains the failure they fix and the reasoning behind them.
  • Changed the transfer wizard's destination list to mirror the routes the backend will actually execute. It derived the list as "every type except the source", which is not the rule — FIAT and SPOT cannot reach FUTURES, and the POST refuses the pair outright with 400 Invalid wallet type transfer. So the wizard offered Futures for a fiat or spot balance, rendered the target-currency step and the amount step for it, and only refused at the final click.
  • Changed both currency fetches in the transfer wizard to a last-selection-wins guard. Clicks arrive faster than round trips, and whichever request the network answered last used to win — so an empty Eco answer landing after a populated Spot one blanked a list that had just been filled in. The guard covers teardown too: pressing "Start over" mid-fetch could otherwise paint "Failed to fetch currencies" above a freshly reset form with nothing pending.
  • Changed the withdraw wizard to probe its three wallet types concurrently instead of one after another, and to wait for the config to arrive before probing at all. The wallet types to probe are derived from settings that start empty, so running early made Promise.all([]) resolve immediately with nothing probed and step 1 render "No Wallets Available" as a settled answer — then, when the config landed, switch to a spinner and then to wallet pills, which reads as a glitching page rather than a loading one.
  • Changed blockchains/{sol,ton,tron,xmr}.ts, the BTC deposit scanner, the spot deposit intent, the price-alert mark-price loader, the custom-chain lookup and the spot custody cron to use the optional-import helpers. Beyond the boot crash, the bare catch {} they replace was flattening "addon installed but broken" into "addon absent" — leaving bindings undefined with no record, so the failure surfaced much later as x is not a function with nothing pointing at the cause. Two of them were also logging Cannot find module ... into the admin cron log on every run of every install without the addon; that noise is gone, while a real defect inside an installed addon still throws and still lands there.
  • Changed the pool-backing custody reads to return an explicit error when the Ecosystem addon is absent, rather than a zero. A figure nobody read is not a balance of nothing.

Fixed

An empty list was read as a boolean, in every finance wizard

An empty list has four causes — not fetched yet, fetched and genuinely empty, the request failed, and superseded by a newer selection — and every step collapsed them into one. The state a failed fetch leaves behind is byte-for-byte the state a genuinely empty answer leaves behind, so the wizard stated as fact that the customer owns no wallets while the error banner directly above it said the request had failed: two contradictory explanations, one of them false.

It was also a false dead-end on every single click, not just on failures. The fetches live in effects, so they start after the render that commits the selection — and that render is "a wallet is selected, the list is empty, nothing is pending", which is exactly what the empty state draws.

  • Fixed by raising the in-flight flag in the same set() that empties the list, so no render can land in between, and by giving each list its own error slot instead of sharing one with balance failures and server refusals.
  • Fixed the deposit method panel to treat null as "not asked yet" and [] as "asked, and there are none". It previously required loading to be true to show skeletons, so the panel dropped out of the page entirely for one frame; and for FIAT it rendered a heading with blank space under it, because every group was gated on its own length and an empty array passed the truthiness test that would have set an error.
  • Fixed the transfer wizard's source-wallet change to clear everything downstream of it. It cleared three fields and left eleven, and toCurrency surviving was the damaging one: the step-5 gate is transferType === "wallet" && toCurrency, so after switching source wallets the amount panel stayed mounted showing the previous wallet's available balance, the previous amount and a stale rate, beside an empty ticker.
  • Fixed setFromCurrency being declared as returning void while its implementation was async and also fetched the balance. The one caller that needed to await it could not see that it should, and fired a second identical balance request instead.

The currency route's empty answer did not keep the shape of its full answer

Covered in Upgrade Notes.

  • Fixed all four short-circuit paths to answer {from, to} for action=transfer. This is the backend half of the blank transfer step above.
  • Fixed the route's declared response schema, which matched no path in the file.
  • Fixed the Eco branch to order its currencies deterministically.

A malformed rate-limit setting disabled the limiter

Covered in Upgrade Notes.

  • Fixed both settings to fall back to their defaults on anything that is not a finite positive number, with one warning per variable per process — rate-limited because this runs on every request and a misconfiguration that printed a line each time would be its own outage.

The platform's own logging was taking the event loop

Covered in Upgrade Notes.

  • Fixed the non-TTY per-step log to print only steps that warned or failed unless LOG_LEVEL=debug is set. The TTY branch is untouched.
  • Fixed the wallet audit logger's console copy to be gated on the log level, and the level checked before the JSON.stringify — an argument is built whether or not the call prints it. The durable row is unaffected.