diff options
| author | Adam Soltys <[email protected]> | 2026-05-17 23:23:14 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-05-17 23:23:14 -0700 |
| commit | 82e43982ca49cdec054bbd31c0e739730fc213f2 (patch) | |
| tree | 64e3ffcd195293c4fe98c586a384565f96c47e09 /lib/payments.ts | |
| parent | 0e368dab0943067c060ad45578a4c4cbdc3434a6 (diff) | |
| download | coinos-server-82e43982ca49cdec054bbd31c0e739730fc213f2.tar.gz coinos-server-82e43982ca49cdec054bbd31c0e739730fc213f2.zip | |
move UTXO lockUnspent from build() to sendOnchain()
build() is called from both /payments (the actual send path) and /fee
(the fee-estimation endpoint). Locking UTXOs in build() meant that every
fee quote was reserving inputs that never got broadcast, slowly leaking
UTXO locks until a bitcoind restart cleared them — eventually starving
the wallet of selectable inputs even when balance was healthy.
The lock belongs at the point where we're actually about to broadcast,
which is sendOnchain. Released in the catch path on broadcast failure;
on success, the inputs become spent and the lock is moot.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Diffstat (limited to 'lib/payments.ts')
| -rw-r--r-- | lib/payments.ts | 19 |
1 files changed, 9 insertions, 10 deletions
diff --git a/lib/payments.ts b/lib/payments.ts index 5d2bf0e6..e0040217 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -448,6 +448,15 @@ export const sendOnchain = async (params) => { if (inflight[txid]) fail("payment in flight"); inflight[txid] = true; + // Reserve UTXOs to keep concurrent sends from selecting the same inputs. + // Released in the catch block on failure; spent UTXOs make the lock moot on success. + try { + const lockVin = tx.vin.map(({ txid, vout }) => ({ txid, vout })); + if (lockVin.length) await node.lockUnspent(false, lockVin); + } catch (e: any) { + warn("lockUnspent failed", e.message); + } + if (!signed) { if (config[type].walletpass) await node.walletPassphrase(config[type].walletpass, 300); @@ -803,16 +812,6 @@ export const build = async ({ inputs.push({ witnessUtxo, path }); } - // Reserve selected UTXOs so concurrent builds don't pick the same inputs. - // Locks are in-memory only; cleared on bitcoind restart. sendOnchain unlocks on broadcast failure. - try { - await node.lockUnspent( - false, - vin.map(({ txid, vout }) => ({ txid, vout })), - ); - } catch (e: any) { - warn("lockUnspent failed", e.message); - } return { feeRate, ourfee, fee, fees, hex: tx.hex, inputs, subtract }; }; |
