aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-05-17 23:23:14 -0700
committerAdam Soltys <[email protected]>2026-05-17 23:23:14 -0700
commit82e43982ca49cdec054bbd31c0e739730fc213f2 (patch)
tree64e3ffcd195293c4fe98c586a384565f96c47e09
parent0e368dab0943067c060ad45578a4c4cbdc3434a6 (diff)
downloadcoinos-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]>
-rw-r--r--lib/payments.ts19
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 };
};