diff options
| author | Adam Soltys <[email protected]> | 2026-06-26 14:02:56 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-26 14:02:56 -0700 |
| commit | f8834d6559cc8bc133d365bb2b3c625590483a64 (patch) | |
| tree | 793c094df4e06c2f9c6d8fab1fa0e628c0e1fbe6 /src | |
| parent | bb939f73e4e02b528e346b9f9d909c3a25cfedbb (diff) | |
| download | coinosv3-f8834d6559cc8bc133d365bb2b3c625590483a64.tar.gz coinosv3-f8834d6559cc8bc133d365bb2b3c625590483a64.zip | |
Fix stale cross-device balance + Electrum call timeouts
Two Electrum-backend issues:
1. Stale balance after a send made on another device: _reconcileHeld set each
address's balance from get_balance/address-stats, where a confirmed coin being
spent by an unconfirmed tx STILL counts as confirmed (with a compensating
negative unconfirmed). So a device that didn't make the send kept showing the
spent coin's value. Now the entry balance is derived from the actual unspent
set (listunspent, which excludes spent coins) — verified a spent coin clears.
2. Per-address rescan failing with 'electrum timeout: blockchain.scripthash.
get_balance': public Electrum servers can drop a response or the socket can
flap mid-call. _rpcCall now retries a couple of times on a fresh id (12s each)
before giving up.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/wallet.js | 33 |
1 files changed, 17 insertions, 16 deletions
diff --git a/src/wallet.js b/src/wallet.js index c1ebc74..550d74d 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -481,22 +481,19 @@ export class Wallet { } catch { continue; } - if (entry) { - entry.confirmed = confirmed; - entry.pending = pending; - } + // Derive the entry's balance from the ACTUAL unspent coins, not from + // get_balance/address-stats: a confirmed coin that's being spent by an + // unconfirmed tx still shows as "confirmed" there (with a compensating + // negative unconfirmed), so trusting it would keep a just-spent coin in the + // headline balance on another device. listunspent excludes spent coins. this.utxos = this.utxos.filter((u) => u.address !== address); + let eConf = 0, ePend = 0; for (const u of us) { - this.utxos.push({ - txid: u.txid, - vout: u.vout, - value: u.value, - address, - chain: p.chain, - index: p.index, - confirmed: !!u.status.confirmed, - }); + const conf = !!(u.status && u.status.confirmed); + if (conf) eConf += u.value; else ePend += u.value; + this.utxos.push({ txid: u.txid, vout: u.vout, value: u.value, address, chain: p.chain, index: p.index, confirmed: conf }); } + if (entry) { entry.confirmed = eConf; entry.pending = ePend; } changed = true; } return changed; @@ -1586,14 +1583,18 @@ export class Wallet { // when the matching id comes back; queues until the socket is open, and times // out so a wedged call can't hang a scan. ids are monotonic for the session so // they never alias a fire-and-forget _rpcSend id. - _rpcCall(method, params) { + _rpcCall(method, params, attempt = 0) { return new Promise((resolve, reject) => { if (this.offline) { reject(new Error('offline')); return; } const id = ++this._rpcId; if (!this._pending) this._pending = new Map(); const timer = setTimeout(() => { - if (this._pending.delete(id)) reject(new Error('electrum timeout: ' + method)); - }, 20000); + if (!this._pending.delete(id)) return; + // A public Electrum server can drop a response, or the socket can flap + // mid-call — retry a couple of times on a fresh id before giving up. + if (attempt < 2 && !this.offline) this._rpcCall(method, params, attempt + 1).then(resolve, reject); + else reject(new Error('electrum timeout: ' + method)); + }, 12000); this._pending.set(id, { resolve, reject, timer }); const payload = JSON.stringify({ id, method, params }) + '\n'; if (this._ws && this._ws.readyState === 1) { |
