diff options
| author | Adam Soltys <[email protected]> | 2026-06-26 13:57:27 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-26 13:57:27 -0700 |
| commit | bb939f73e4e02b528e346b9f9d909c3a25cfedbb (patch) | |
| tree | 8fe13e6b2dc644bbf13ee3fba0d47877b0450f96 /src/wallet.js | |
| parent | 9f4bc29025287e8e12a7ef31c9918c1adfdfff5d (diff) | |
| download | coinosv3-bb939f73e4e02b528e346b9f9d909c3a25cfedbb.tar.gz coinosv3-bb939f73e4e02b528e346b9f9d909c3a25cfedbb.zip | |
Update balance/history optimistically right after a send
On the Electrum backend the post-send rescan is many round-trips, so the balance
lagged a few seconds. Now a successful broadcast immediately applies the tx
locally (applySentTx): drop the spent coins, credit change as pending, add a
pending history row, recompute the balance, and emit — so the UI reflects the
send instantly. The realtime watcher / backstop poll reconciles and fills in the
confirmation once the explorer has indexed it (no immediate rescan, so a
not-yet-indexed tx can't briefly revert the optimistic state). Verified the
balance/utxo/history math.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/wallet.js')
| -rw-r--r-- | src/wallet.js | 52 |
1 files changed, 52 insertions, 0 deletions
diff --git a/src/wallet.js b/src/wallet.js index b4aff8b..c1ebc74 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1378,6 +1378,58 @@ export class Wallet { return true; } + // Optimistically reflect a just-broadcast outgoing tx so the balance and + // history update instantly instead of waiting for the post-send rescan: drop + // the spent coins, credit our change as pending, and add a pending history + // row. The realtime watcher / backstop poll reconciles it (and fills in the + // confirmation) once the explorer has indexed the tx. `tx` is the finalized + // btc.Transaction we just signed and broadcast. + applySentTx(tx) { + const txid = tx.id; + if (this.txs.find((t) => t.txid === txid)) return; // already applied + const byOutpoint = new Map(); + for (const u of this.utxos) byOutpoint.set(utxoId(u), u); + + let inSum = 0; + const spent = new Set(); + for (let i = 0; i < tx.inputsLength; i++) { + const inp = tx.getInput(i); + const id = `${hex.encode(inp.txid)}:${inp.index}`; + const u = byOutpoint.get(id); + if (!u) continue; + inSum += u.value; + spent.add(id); + const e = (u.chain === 0 ? this.receive : this.change).find((a) => a.index === u.index); + if (e) { if (u.confirmed) e.confirmed = Math.max(0, (e.confirmed || 0) - u.value); else e.pending = Math.max(0, (e.pending || 0) - u.value); } + } + this.utxos = this.utxos.filter((u) => !spent.has(utxoId(u))); + + let outSum = 0, change = 0; + for (let i = 0; i < tx.outputsLength; i++) { + const o = tx.getOutput(i); + outSum += Number(o.amount); + let addr; + try { addr = btc.Address(this.netCfg.net).encode(btc.OutScript.decode(o.script)); } catch {} + const info = addr && this.addrMap.get(addr); + if (!info) continue; // recipient output — not ours + this.utxos.push({ txid, vout: i, value: Number(o.amount), address: addr, chain: info.chain, index: info.index, confirmed: false }); + const e = (info.chain === 0 ? this.receive : this.change).find((a) => a.index === info.index); + if (e) { e.pending = (e.pending || 0) + Number(o.amount); e.used = true; } + change += Number(o.amount); + } + + this.txs.unshift({ txid, net: change - inSum, fee: inSum - outSum, vsize: tx.weight ? Math.ceil(tx.weight / 4) : 0, confirmed: false, blockTime: 0, blockHeight: 0, firstSeen: Date.now() }); + this.nextReceiveIndex = firstUnused(this.receive); + this.nextChangeIndex = firstUnused(this.change); + this.utxos.sort((a, b) => b.value - a.value); + this._sortTxs(); + this._recomputeBalanceFromUtxos(); + this.loaded = true; + this.saveCache(); + this.retrack(); + this.emit(); + } + startRealtime() { if (this.offline) return; this.stopRealtime(); |
