aboutsummaryrefslogtreecommitdiffstats
path: root/src/wallet.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-27 13:53:00 -0700
committerAdam Soltys <[email protected]>2026-06-27 13:53:00 -0700
commite27b5ac2666ada59b97bbb52dea76dcc58ace13f (patch)
tree799cd97e989c2706cdfaa36a069cb42ec97fda64 /src/wallet.js
parent381ca347bab721965c682a630ed61a7c236ebed5 (diff)
parent1dc0c188d38c9d291375601c25a0533924402359 (diff)
downloadcoinosv3-e27b5ac2666ada59b97bbb52dea76dcc58ace13f.tar.gz
coinosv3-e27b5ac2666ada59b97bbb52dea76dcc58ace13f.zip
Merge branch 'master' into boltz-swaps
# Conflicts: # src/app.js
Diffstat (limited to 'src/wallet.js')
-rw-r--r--src/wallet.js35
1 files changed, 25 insertions, 10 deletions
diff --git a/src/wallet.js b/src/wallet.js
index bc24393..a92b0ce 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -21,7 +21,7 @@ import * as btc from '@scure/btc-signer';
import { p2wpkh } from '@scure/btc-signer/payment';
import { concatBytes } from '@scure/btc-signer/utils.js';
-import { Api, pool, wsUrl, getBackend, explorerWeb, electrumCandidates } from './api.js';
+import { Api, pool, getBackend, explorerWeb, electrumCandidates } from './api.js';
import { ElectrumApi } from './electrum.js';
import { NostrSync, getSyncConfig } from './nostr.js';
import { isSilentPaymentAddress, decodeSilentPaymentAddress, silentPaymentScripts, silentPaymentPlaceholder } from './silentpay.js';
@@ -41,6 +41,9 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const NETS = {
mainnet: { net: btc.NETWORK, coin: 0 },
testnet: { net: btc.TEST_NETWORK, coin: 1 },
+ // mutinynet is a signet — same address format / version bytes / coin type as
+ // testnet (`tb` HRP); only the chain + servers differ.
+ mutinynet: { net: btc.TEST_NETWORK, coin: 1 },
// Regtest shares testnet's version bytes / coin type; only the bech32 HRP
// differs (`bcrt`). @scure/btc-signer derives addresses + validates from this.
regtest: { net: { ...btc.TEST_NETWORK, bech32: 'bcrt' }, coin: 1 },
@@ -703,6 +706,9 @@ export class Wallet {
_sortTxs() {
this.txs.sort((a, b) => {
if (a.confirmed !== b.confirmed) return a.confirmed ? 1 : -1; // pending first
+ // Pending txs share blockTime 0, so order them by when we first saw them
+ // (newest on top); confirmed txs go by block time (newest on top).
+ if (!a.confirmed) return (b.firstSeen || 0) - (a.firstSeen || 0);
return (b.blockTime || 0) - (a.blockTime || 0);
});
}
@@ -1288,14 +1294,14 @@ export class Wallet {
// Pushes us new mempool/confirmed transactions for our addresses so history
// and balances update with no polling.
wsUrl() {
- // Electrum backend: pick the current failover candidate (rotated on failure).
- // Mainnet (when cut over to Fulcrum) and regtest (local Fulcrum) use Electrum.
- if (getBackend() === 'electrum' && (this.netName === 'mainnet' || this.netName === 'regtest')) {
- if (!this._wsCandidates || !this._wsCandidates.length) { this._wsCandidates = electrumCandidates(); this._wsCandIdx = 0; }
+ // Electrum backend (any network): pick the current failover candidate for the
+ // active network (rotated on failure). Esplora backend has no push → poll.
+ if (getBackend() === 'electrum') {
+ if (!this._wsCandidates || !this._wsCandidates.length) { this._wsCandidates = electrumCandidates(this.netName); this._wsCandIdx = 0; }
const list = this._wsCandidates;
return list.length ? list[(this._wsCandIdx || 0) % list.length] : null;
}
- return wsUrl(this.netName); // coinos watcher, or null
+ return null;
}
// What realtime watches: the fresh frontier (next receive + next change), plus
@@ -1498,16 +1504,18 @@ export class Wallet {
if (typeof WebSocket === 'undefined' || !this.wsUrl()) return;
this._wsWant = true;
- // Heartbeat: ping every 15s; if no message (incl. pong) for 45s the socket
- // is half-open (died without firing onclose) — force a reconnect.
+ // Heartbeat: ping every 10s; if nothing comes back (incl. our own pong) for
+ // 21s the socket is half-open (died without firing onclose) — recycle it.
+ // The trigger is two unanswered pings, so one slow round-trip won't churn,
+ // but a silently-dead socket now recovers in ~20s instead of ~45s.
this._hbTimer = setInterval(() => {
if (this.offline || !this._ws || this._ws.readyState !== 1) return;
- if (Date.now() - this._lastMsg > 45000) {
+ if (Date.now() - this._lastMsg > 21000) {
this._reconnectNow();
} else {
this._rpcSend('server.ping', []);
}
- }, 15000);
+ }, 10000);
// Reconnect + refresh when the tab refocuses or the network returns.
if (!this._wakeHooked && typeof document !== 'undefined') {
@@ -1535,6 +1543,8 @@ export class Wallet {
} catch {}
this._ws = null;
}
+ // onclose was nulled above so it won't fire — fail in-flight calls here too.
+ this._rejectPending('socket recycled');
this.live = false;
this._connectWs();
}
@@ -1598,6 +1608,10 @@ export class Wallet {
ws.onclose = () => {
this.live = false;
this._ws = null;
+ // Fail any in-flight data calls so the _rpcCall pump frees its slots
+ // immediately; otherwise dead calls hold the pump until they time out and
+ // the post-reconnect refresh stalls behind them (~10s of stale UI).
+ this._rejectPending('socket closed');
this.emit();
this._scheduleReconnect();
};
@@ -1810,6 +1824,7 @@ export class Wallet {
this.change = d.change || [];
this.utxos = d.utxos || [];
this.txs = d.txs || [];
+ this._sortTxs(); // a cache saved before the firstSeen-ordering fix may be stale
this.nextReceiveIndex = d.nextReceiveIndex || 0;
this.nextChangeIndex = d.nextChangeIndex || 0;
this.feeRates = d.feeRates || this.feeRates;