From ff69e7e9f166e01102500b559b0cd66e3a019fab Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 26 Jun 2026 08:23:57 -0700 Subject: Add Electrum-over-WebSocket as an alternative data backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/api.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) (limited to 'src/api.js') 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 { -- cgit v1.2.3