From 82e43982ca49cdec054bbd31c0e739730fc213f2 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 17 May 2026 23:23:14 -0700 Subject: move UTXO lockUnspent from build() to sendOnchain() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- lib/payments.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) (limited to 'lib') 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 }; }; -- cgit v1.2.3