aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-06 14:55:40 -0700
committerAdam Soltys <[email protected]>2026-07-06 14:55:40 -0700
commit312621af84136fddbe7d7e47ffecc1ba2b38920d (patch)
tree6090489ef5ae7bac16a25340bd2c11f426079cbb /src
parentd071a09604599831d370b06563144cf4ba028902 (diff)
downloadcoinosv3-312621af84136fddbe7d7e47ffecc1ba2b38920d.tar.gz
coinosv3-312621af84136fddbe7d7e47ffecc1ba2b38920d.zip
Fee rates from live explorer + reconnect half-open socket on wake
- Fee rates on the electrum backend came from Fulcrum's estimatefee (Core's estimatesmartfee): historical, laggy, floored at 1 sat/vB, so it showed 1 across the board while mempool.space showed 11/10. Pull the configured explorer's live recommendation instead (/v1/fees/recommended, then /fee-estimates), falling back to estimatefee only when the explorer is unreachable — self-hosted/regtest behaviour unchanged. - On mobile a backgrounded page leaves the socket half-open: TCP is dead but readyState still reads OPEN, so wake() never reconnected and fired scan RPCs into the void, waiting ~20s for the heartbeat to recover. wake() now forces a reconnect when the socket has been silent past the heartbeat window, so a payment shows the instant you refocus. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/api.js2
-rw-r--r--src/electrum.js21
-rw-r--r--src/wallet.js10
3 files changed, 31 insertions, 2 deletions
diff --git a/src/api.js b/src/api.js
index 76852d4..4a7dd75 100644
--- a/src/api.js
+++ b/src/api.js
@@ -593,7 +593,7 @@ export class Api {
// Esplora /fee-estimates is { blockTarget: sat/vB }. Map to the named tiers the
// UI expects. Round up so we never underpay.
-function mapEsploraFees(est) {
+export function mapEsploraFees(est) {
const at = (n, d) => Math.max(1, Math.ceil(est[n] || d));
return {
fastestFee: at(1, 10),
diff --git a/src/electrum.js b/src/electrum.js
index b8ca164..fb61298 100644
--- a/src/electrum.js
+++ b/src/electrum.js
@@ -16,6 +16,7 @@
import * as btc from '@scure/btc-signer';
import { sha256 } from '@noble/hashes/sha256';
import { hexToBytes, bytesToHex } from '@noble/hashes/utils';
+import { mapEsploraFees } from './api.js';
const RAW_OPTS = { allowUnknownOutputs: true, allowUnknownInputs: true, disableScriptCheck: true };
@@ -214,7 +215,27 @@ export class ElectrumApi {
}
// ---- fees + broadcast --------------------------------------------------
+ // Fee rates come from the configured block explorer's live recommendation
+ // (mempool.space projects the next block from actual mempool contents), NOT
+ // from Fulcrum's blockchain.estimatefee — that wraps Bitcoin Core's
+ // estimatesmartfee, which works off historical confirmation data, lags the
+ // mempool badly, and floors at 1 sat/vB, so it reports 1 across the board in
+ // normal conditions. estimatefee is only the last resort when the explorer is
+ // unreachable (regtest, or a self-hosted node with no REST explorer).
async feeRates() {
+ const web = this._explorerWeb;
+ if (web && !this.offline) {
+ // mempool.space shape first (the live next-block projection)…
+ try {
+ const r = await fetch(`${web}/api/v1/fees/recommended`);
+ if (r.ok) { const d = await r.json(); if (d && d.halfHourFee) return d; }
+ } catch {}
+ // …then the plain-Esplora /fee-estimates shape.
+ try {
+ const r = await fetch(`${web}/api/fee-estimates`);
+ if (r.ok) { const d = await r.json(); if (d && Object.keys(d).length) return mapEsploraFees(d); }
+ } catch {}
+ }
const targets = { fastestFee: 1, halfHourFee: 3, hourFee: 6, economyFee: 144, minimumFee: 1008 };
const out = {};
await Promise.all(Object.entries(targets).map(async ([k, n]) => {
diff --git a/src/wallet.js b/src/wallet.js
index 1c58de6..2417383 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -1520,7 +1520,15 @@ export class Wallet {
this._wakeHooked = true;
const wake = () => {
if (this.offline || !this._wsWant) return;
- if (!this._ws || this._ws.readyState !== 1) this._reconnectNow();
+ // A backgrounded mobile page often leaves the socket HALF-OPEN: the OS
+ // killed the TCP connection but the frozen JS runtime never got onclose,
+ // so readyState still reads 1 (OPEN). Don't trust it — if the socket has
+ // been silent past the heartbeat window it's dead, so force a reconnect
+ // rather than firing scan RPCs into the void and waiting ~20s for the
+ // heartbeat to notice. A healthy foreground socket pings every 10s, so
+ // _lastMsg stays fresh and this never churns a live connection.
+ const stale = Date.now() - (this._lastMsg || 0) > 15000;
+ if (!this._ws || this._ws.readyState !== 1 || stale) this._reconnectNow();
this.scan({ silent: true }).catch(() => {});
};
document.addEventListener('visibilitychange', () => {