From b6875f7bcefe10c4c23868d7952c4584d557aa60 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 4 Jul 2026 23:07:51 -0700 Subject: Prune RBF-replaced pending txs from history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A replaced/evicted unconfirmed tx lingered forever as a phantom pending receive (signetfaucet fee-bumps its batch payout every few minutes, so History grew a new copy per bump). The poll now status-checks pending txs — via /tx/:txid, since /tx/:txid/status answers 200 confirmed:false for txids the node no longer knows — drops the vanished ones, and re-fetches their addresses' coins and history, which _reconcileHeld skips when the replacement pays identical amounts. Co-Authored-By: Claude Fable 5 --- src/api.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'src/api.js') diff --git a/src/api.js b/src/api.js index e011e9c..76852d4 100644 --- a/src/api.js +++ b/src/api.js @@ -486,7 +486,7 @@ export class Api { return null; } - async #get(path, asText = false) { + async #get(path, asText = false, { notFoundNull = false } = {}) { if (this.offline) throw new Error('offline'); let lastErr; for (let attempt = 0; attempt < 6; attempt++) { @@ -499,6 +499,7 @@ export class Api { } try { const res = await this.#run(host, path); + if (notFoundNull && res.status === 404) return null; // definitive: not an error if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${path}`); return asText ? await res.text() : await res.json(); } catch (e) { @@ -533,6 +534,16 @@ export class Api { return this.#get(`/tx/${txid}`); } + // Confirmation status of one tx, or null when the node no longer knows the + // txid at all — i.e. it was RBF-replaced or evicted from the mempool. The + // 404 is the answer we're after, so it doesn't retry or fail over. NOTE: + // /tx/:txid/status is NOT usable for this — electrs and mempool.space both + // answer 200 {confirmed:false} for unknown txids; only /tx/:txid 404s. + async txStatus(txid) { + const tx = await this.#get(`/tx/${txid}`, false, { notFoundNull: true }); + return tx ? tx.status || { confirmed: false } : null; + } + async feeRates() { for (const host of this._hosts) { if (host.cooldownUntil > Date.now()) continue; -- cgit v1.2.3