From a2bb363e2c86dc845e6748e046c8d489e656c584 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 25 Jun 2026 09:39:14 -0700 Subject: Realtime via self-hosted ZMQ watcher (Electrum-over-WS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the unreliable public WebSocket push (mempool/blockchain.info address subscriptions never reliably fire) with a private notification service: - tools/zmq-watcher.js: a standalone Bun service (reusing coinos lib/zmq.ts's ZMTP subscriber) that reads Bitcoin Core's ZMQ rawtx/rawblock, computes the scripthash of each output, and pushes a blockchain.scripthash.subscribe notification to subscribed clients. No historical index (~24MB RAM) — just the live tx stream. Runs on the coinos box reading its Core's ZMQ. - wallet.js: realtime client now speaks the Electrum protocol (server.version, scripthash.subscribe, server.ping) over the WS; any scripthash notification triggers the existing refreshLive() (REST stays the source of truth). - api.js: wsUrl -> wss://coinos.io/electrum. Verified end-to-end: live mempool txs to subscribed addresses notify instantly. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/api.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) (limited to 'src/api.js') diff --git a/src/api.js b/src/api.js index 08f03b2..20041e2 100644 --- a/src/api.js +++ b/src/api.js @@ -45,11 +45,24 @@ function resolveHosts(net) { return [host('https://mempool.space', 'mempool'), host('https://blockstream.info', 'esplora')]; } -// WebSocket URL for live updates — only mempool.space exposes one, so other -// explorers fall back to polling (returns null → no socket). +// Realtime is a private Fulcrum (Electrum protocol over WebSocket-Secure). It +// reliably pushes a scripthash-status notification the moment a tx touches a +// watched address — unlike the public Esplora/blockchain.info sockets, whose +// address push doesn't fire. Independent of the REST explorer choice. Mainnet +// only; testnet falls back to polling (null). Overridable for staging/testing. +const ELECTRUM_KEY = 'btc-wallet-electrum'; +export function getElectrumWsUrl() { + try { + const u = localStorage.getItem(ELECTRUM_KEY); + if (u) return u; + } catch {} + // Served as a path under coinos.io (a dedicated subdomain hit a Cloudflare + // WebSocket quirk; the main hostname proxies WS cleanly). + return 'wss://coinos.io/electrum'; +} export function wsUrl(net) { - if (getExplorerConfig().server !== 'mempool') return null; - return 'wss://mempool.space' + (net === 'testnet' ? '/testnet/api/v1/ws' : '/api/v1/ws'); + if (net === 'testnet') return null; + return getElectrumWsUrl(); } const REQUEST_TIMEOUT_MS = 10000; -- cgit v1.2.3