aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-26 14:02:56 -0700
committerAdam Soltys <[email protected]>2026-06-26 14:02:56 -0700
commitf8834d6559cc8bc133d365bb2b3c625590483a64 (patch)
tree793c094df4e06c2f9c6d8fab1fa0e628c0e1fbe6 /src
parentbb939f73e4e02b528e346b9f9d909c3a25cfedbb (diff)
downloadcoinosv3-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.js33
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) {