aboutsummaryrefslogtreecommitdiffstats
path: root/src/api.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-26 08:23:57 -0700
committerAdam Soltys <[email protected]>2026-06-26 08:23:57 -0700
commitff69e7e9f166e01102500b559b0cd66e3a019fab (patch)
treed3e26d58118559f59d69222c852dcb01b41f1899 /src/api.js
parentaa044138337c947b26eea910db57f2d5b743b5b8 (diff)
downloadcoinosv3-ff69e7e9f166e01102500b559b0cd66e3a019fab.tar.gz
coinosv3-ff69e7e9f166e01102500b559b0cd66e3a019fab.zip
Add Electrum-over-WebSocket as an alternative data backend
Settings → Data source now offers 'Electrum' alongside the Esplora REST explorer. One Electrum WebSocket serves BOTH the wallet's data queries and scripthash push notifications, so pointing Hal at your own Fulcrum/electrs gives a fully self-hosted setup — no coinos, no public REST, nothing shared off your node. - src/electrum.js: ElectrumApi maps the Esplora shapes the wallet consumes onto Electrum methods (scripthash get_balance/listunspent/get_history, estimatefee, transaction.broadcast). electrs has no verbose tx lookup, so it parses raw txs and resolves input values from parent txs to compute fees (cached, in-flight deduped, bounded-parallel). Validated field-for-field against mempool.space: addressTxs (fee/vin/vout/status), getTx, outspend, balance, utxos. - wallet.js: the realtime socket already spoke Electrum's notify protocol; added request/response (_rpcCall) so the same connection answers data calls. Real Electrum notifications carry no tx data, so they reconcile via the backend. Replaced the fragile newline parser with a string-aware brace-depth scanner that handles every framing (newline, one-per-frame, concatenated, split) — fixes a buffer-poisoning hang under concurrent calls; also hardens the coinos watcher path. Esplora stays the default; nothing changes for existing users. Verified end-to-end against blockstream's electrs: concurrent transport, a full fresh-wallet scan (~2s), and the parser unit-tested on all framings. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/api.js')
-rw-r--r--src/api.js47
1 files changed, 46 insertions, 1 deletions
diff --git a/src/api.js b/src/api.js
index b981bc6..feac78f 100644
--- a/src/api.js
+++ b/src/api.js
@@ -71,11 +71,56 @@ export function setRealtimeEnabled(on) {
try { localStorage.setItem(REALTIME_KEY, on ? 'on' : 'off'); } catch {}
}
+// Data backend: the default Esplora REST (`Api`) or a full Electrum-over-WS
+// server (`ElectrumApi`) that serves data AND notifications over one connection.
+const BACKEND_KEY = 'btc-wallet-backend';
+export function getBackend() {
+ try { return localStorage.getItem(BACKEND_KEY) === 'electrum' ? 'electrum' : 'esplora'; } catch { return 'esplora'; }
+}
+export function setBackend(b) {
+ try { localStorage.setItem(BACKEND_KEY, b === 'electrum' ? 'electrum' : 'esplora'); } catch {}
+}
+
+// Electrum servers that accept WebSocket connections. blockstream.info is
+// electrs (validated); the point, though, is pointing at your own Fulcrum/electrs.
+export const ELECTRUM_PRESETS = [
+ { id: 'blockstream', label: 'blockstream.info', url: 'wss://blockstream.info/electrum-websocket/' },
+ { id: 'jochen', label: 'electrum.jochen-hoenicke.de', url: 'wss://electrum.jochen-hoenicke.de:50010' },
+ { id: 'custom', label: 'Custom (your node)', url: '' },
+];
+const ELECTRUM_SERVER_KEY = 'btc-wallet-electrum-server';
+export function getElectrumServerConfig() {
+ try {
+ const c = JSON.parse(localStorage.getItem(ELECTRUM_SERVER_KEY) || 'null');
+ if (c && c.server) return { server: c.server, url: c.url || '' };
+ } catch {}
+ return { server: 'blockstream', url: '' };
+}
+export function setElectrumServerConfig({ server, url }) {
+ try { localStorage.setItem(ELECTRUM_SERVER_KEY, JSON.stringify({ server, url: url || '' })); } catch {}
+}
+export function resolveElectrumUrl() {
+ const cfg = getElectrumServerConfig();
+ if (cfg.server === 'custom') return cfg.url.trim() || null;
+ const p = ELECTRUM_PRESETS.find((x) => x.id === cfg.server);
+ return (p && p.url) || ELECTRUM_PRESETS[0].url;
+}
+
export function wsUrl(net) {
- if (net === 'testnet' || !getRealtimeEnabled()) return null;
+ if (net === 'testnet') return null;
+ // Electrum backend: the socket IS the data source, so it's always wanted
+ // (independent of the coinos-notification opt-out, which only governs Esplora).
+ if (getBackend() === 'electrum') return resolveElectrumUrl();
+ if (!getRealtimeEnabled()) return null;
return getElectrumWsUrl();
}
+// Web base for tx links (Electrum has no web UI — reuse the chosen explorer host).
+export function explorerWeb() {
+ const hosts = resolveHosts('mainnet');
+ return (hosts[0] && hosts[0].web) || 'https://mempool.space';
+}
+
const REQUEST_TIMEOUT_MS = 10000;
export class Api {