aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-31 09:28:48 -0700
committerAdam Soltys <[email protected]>2026-08-31 09:28:48 -0700
commit193f49a7a4fb73ef7bfa405c17e8da3ab94f4205 (patch)
tree54f7ed13d35cc58fea45e20504fc09e538c4f3e7 /src
parent29b0dddad8edde818d0257f39988c9e82f387290 (diff)
downloadcoinosv3-193f49a7a4fb73ef7bfa405c17e8da3ab94f4205.tar.gz
coinosv3-193f49a7a4fb73ef7bfa405c17e8da3ab94f4205.zip
The chain adapter learns a second road: mainnet esplora calls fail over between the configured host, blockstream.info, and mempool.space (remembering whichever answered last, rotating on network failures, 429s and 5xx) — one tester's network simply cannot reach mempool.space and every chain-touching feature was dying for it, stranded mailbox receives included. And a failed quote no longer wedges the Lightning pay form at an eternal spinner: it prices at the outage fallback and lets Pay proceed, where the payment path quotes again for real
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
Diffstat (limited to 'src')
-rw-r--r--src/ark/manager.js36
-rw-r--r--src/features/ark.js14
2 files changed, 42 insertions, 8 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js
index b5c2aac..e9f79b0 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -190,21 +190,45 @@ export class ArkManager {
_decoded(v) { return decodeVtxo(hex.decode(v.bytes)); }
// ---- chain adapter (esplora REST, same API the wallet already speaks) ----
+ //
+ // With FAILOVER on mainnet: some networks simply can't reach mempool.space
+ // (one tester's laptop failed every chain call for days — stranded mailbox
+ // receives, quotes that never arrived), and a single host made every one
+ // of those a dead end. A request tries the last host that worked, then the
+ // rest; rate limits and 5xx rotate too. Non-mainnet keeps its lone host —
+ // there is no public twin of a regtest or mutinynet esplora.
+ async _esplora(path, init) {
+ const hosts = (this._esploraHosts ||= [
+ this.esploraUrl,
+ ...(this.network === 'mainnet' ? ['https://blockstream.info/api', 'https://mempool.space/api'] : []),
+ ].filter((h, i, a) => h && a.indexOf(h) === i));
+ const start = this._esploraIdx || 0;
+ let lastErr;
+ for (let i = 0; i < hosts.length; i++) {
+ const idx = (start + i) % hosts.length;
+ try {
+ const r = await fetch(hosts[idx] + path, init);
+ if (r.status === 429 || r.status >= 500) { lastErr = new Error(`chain status ${r.status}`); continue; }
+ this._esploraIdx = idx;
+ return r;
+ } catch (e) { lastErr = e; }
+ }
+ throw lastErr;
+ }
get chain() {
- const base = this.esploraUrl;
return {
// no-store: a CDN-cached tip lags a 30s-block network by many blocks,
// and a stale tip under-shoots HTLC expiries the server then rejects
- tipHeight: async () => Number(await fetch(`${base}/blocks/tip/height`, { cache: 'no-store' }).then((r) => r.text())),
+ tipHeight: async () => Number(await this._esplora('/blocks/tip/height', { cache: 'no-store' }).then((r) => r.text())),
getTxStatus: async (txid) => {
- const r = await fetch(`${base}/tx/${txid}/status`);
+ const r = await this._esplora(`/tx/${txid}/status`);
if (r.ok) return r.json();
if (r.status === 404) return null; // unknown tx is an answer
- throw new Error(`chain status ${r.status}`); // rate limits etc must SURFACE
+ throw new Error(`chain status ${r.status}`); // odd refusals must SURFACE
},
- getTxHex: async (txid) => fetch(`${base}/tx/${txid}/hex`).then((r) => r.ok ? r.text() : null),
+ getTxHex: async (txid) => this._esplora(`/tx/${txid}/hex`).then((r) => r.ok ? r.text() : null),
broadcastTx: async (txHex) => {
- const r = await fetch(`${base}/tx`, { method: 'POST', body: txHex });
+ const r = await this._esplora('/tx', { method: 'POST', body: txHex });
const body = await r.text();
// a rebroadcast after a crash is fine — the tx being known already IS success
if (!r.ok && !/already/i.test(body)) throw new Error(`broadcast failed: ${body.slice(0, 120)}`);
diff --git a/src/features/ark.js b/src/features/ark.js
index 2ce26b5..b123458 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -719,8 +719,18 @@ export function arkFeature(ctx) {
ui.arkLnPay = { invoice, meta: meta || null, amountSat: dec.amountSat, amount: '', feeSat: null, status: 'quote' };
ui.sendError = '';
render();
- quoteArkLnPay(invoice, meta).catch((e) => {
- if (ui.arkLnPay && ui.arkLnPay.invoice === invoice) { ui.sendError = e.message; render(); }
+ quoteArkLnPay(invoice, meta).catch(() => {
+ // The quote is a nicety — a dead esplora or quote service must not
+ // wedge the form at an eternal spinner (it did: "Getting a quote…"
+ // above a Failed-to-fetch, Pay unreachable). Price it like the outage
+ // fallback and let Pay proceed; the payment path quotes again with
+ // its own fallbacks and locks only what the route actually needs.
+ const p2 = ui.arkLnPay;
+ if (!p2 || p2.invoice !== invoice) return;
+ p2.feeSat = p2.amountSat ? Math.max(3, Math.ceil(p2.amountSat / 1000)) : null;
+ p2.status = 'ready';
+ ui.sendError = '';
+ render();
});
return true;
}