Area
Wallet
Affected package or service and version
@bsv/wallet-toolbox — src/storage/StorageKnex.ts:489, src/storage/StorageReader.ts:58-62. Current on main at 82ab4d3c; reproduced against a built 2.10.2.
Defect
getRawTxOfKnownValidTransaction() warms the settings cache inline:
// StorageKnex.ts:489
if (!this.isAvailable()) await this.makeAvailable()
makeAvailable() takes no trx at all:
// StorageReader.ts:58-62
async makeAvailable (): Promise<TableSettings> {
if (this._settings != null) return this._settings
this._settings = await this.readSettings()
return this._settings
}
so its readSettings() always runs outside whatever transaction the caller holds — even though StorageKnex.readSettings(trx?) accepts one (:177), and even though getRawTxOfKnownValidTransaction has a trx in scope that it passes to the very next statements (:490, :492).
On SQLite's single-connection pool, a caller already inside a write transaction that reaches this path while the settings cache is cold requests a second connection that can never be granted, and self-deadlocks until acquireConnectionTimeout fires.
This is reachable from inside the package — these all pass a live trx into that method:
src/storage/methods/createAction.ts:1538
src/storage/StorageProvider.ts:1819
src/storage/StorageKnex.ts:1413 → validateRawTransaction(t, args.trx) → :1648
Minimal reproduction
Open a store cold (do not call makeAvailable()), then reach the path from inside a transaction:
const storage = new StorageKnex({ chain, knex, /* ... */ }) // cold: _settings empty
await storage.transaction(async trx => {
await storage.getRawTxOfKnownValidTransaction(txid, undefined, undefined, trx)
})
Expected behavior
Either the settings read joins the caller's transaction (readSettings(trx) already supports it), or the cold-inside-transaction case throws a clear error. Not a connection-pool timeout.
Actual behavior and evidence
Same script twice, differing only in whether makeAvailable() was called at boot first:
COLD (no boot warm): available before trx=false ok=false 5002ms
KnexTimeoutError: Knex: Timeout acquiring a connection. The pool is
probably full. Are you missing a .transacting(trx) call?
WARM (boot warm) : available before trx=true ok=true 1ms
better-sqlite3 + knex 3.3 with acquireConnectionTimeout: 5000. The 5002 ms is the timeout firing, not work being done.
Consumers can only mitigate this by warming settings at boot before serving, which masks the cause rather than fixing it, and silently stops working if any future path reaches a cold instance inside a transaction.
Environment
macOS, Node 22, better-sqlite3 13 / knex 3.3 — the versions @bsv/wallet-toolbox itself declares.
Notes on the fix
Thread trx through the warm on this path — makeAvailable(trx?) delegating to the existing readSettings(trx) — or, at minimum, detect cold-inside-transaction and throw a named error instead of exhausting the pool. verifyReadyForDatabaseAccess(trx?) (:1663) already demonstrates the trx-aware settings read.
Related but separate, and not claimed here as a live defect: countChangeInputs and allocateChangeInput accept no trx and the latter opens its own knex.transaction(...). I traced the callers and found no in-package path reaching them from inside a transaction (createAction's funding allocates in memory over candidates fetched with the caller's trx), so that is an interface-shape concern for outside consumers rather than a reachable deadlock. Mentioned only so it is not mistaken for part of this report.
Area
Wallet
Affected package or service and version
@bsv/wallet-toolbox—src/storage/StorageKnex.ts:489,src/storage/StorageReader.ts:58-62. Current onmainat82ab4d3c; reproduced against a built 2.10.2.Defect
getRawTxOfKnownValidTransaction()warms the settings cache inline:makeAvailable()takes notrxat all:so its
readSettings()always runs outside whatever transaction the caller holds — even thoughStorageKnex.readSettings(trx?)accepts one (:177), and even thoughgetRawTxOfKnownValidTransactionhas atrxin scope that it passes to the very next statements (:490,:492).On SQLite's single-connection pool, a caller already inside a write transaction that reaches this path while the settings cache is cold requests a second connection that can never be granted, and self-deadlocks until
acquireConnectionTimeoutfires.This is reachable from inside the package — these all pass a live
trxinto that method:src/storage/methods/createAction.ts:1538src/storage/StorageProvider.ts:1819src/storage/StorageKnex.ts:1413→validateRawTransaction(t, args.trx)→:1648Minimal reproduction
Open a store cold (do not call
makeAvailable()), then reach the path from inside a transaction:Expected behavior
Either the settings read joins the caller's transaction (
readSettings(trx)already supports it), or the cold-inside-transaction case throws a clear error. Not a connection-pool timeout.Actual behavior and evidence
Same script twice, differing only in whether
makeAvailable()was called at boot first:better-sqlite3+ knex 3.3 withacquireConnectionTimeout: 5000. The 5002 ms is the timeout firing, not work being done.Consumers can only mitigate this by warming settings at boot before serving, which masks the cause rather than fixing it, and silently stops working if any future path reaches a cold instance inside a transaction.
Environment
macOS, Node 22,
better-sqlite313 / knex 3.3 — the versions@bsv/wallet-toolboxitself declares.Notes on the fix
Thread
trxthrough the warm on this path —makeAvailable(trx?)delegating to the existingreadSettings(trx)— or, at minimum, detect cold-inside-transaction and throw a named error instead of exhausting the pool.verifyReadyForDatabaseAccess(trx?)(:1663) already demonstrates the trx-aware settings read.Related but separate, and not claimed here as a live defect:
countChangeInputsandallocateChangeInputaccept notrxand the latter opens its ownknex.transaction(...). I traced the callers and found no in-package path reaching them from inside a transaction (createAction's funding allocates in memory over candidates fetched with the caller'strx), so that is an interface-shape concern for outside consumers rather than a reachable deadlock. Mentioned only so it is not mistaken for part of this report.