// Electrum-sourced BNS indexer — NO node, NO Fulcrum, NO disk, NO sync. // // Instead of scanning blocks from a BCHN node (see RpcSource in bns-indexer.js), // this polls PUBLIC electrum/Fulcrum servers for just the two calls BNS needs — // blockchain.scripthash.get_history (the beacon) + blockchain.transaction.get — // caches the beacon set, and serves the same BNS electrum-over-WebSocket endpoint. // // Result: a featherweight, pinned-IP, DNS-independent caching relay you can run on // a tiny VPS. Tradeoff vs. RpcSource: it trusts the upstream public servers (no own // chain), but it needs no node, no Fulcrum, no storage. // // BNS_ELECTRUM_SERVERS=wss://a:50004,wss://b:50004 (comma-separated, tried in order) // BNS_WS_BIND=127.0.0.1 BNS_WS_PORT=50010 BNS_POLL_MS=30000 // node electrum-source.js import WebSocket from "ws"; import { pathToFileURL } from "node:url"; import { BeaconIndex, startElectrumWS } from "./bns-indexer.js"; const CONFIG = { servers: (process.env.BNS_ELECTRUM_SERVERS || "wss://chipnet.bch.ninja:50004,wss://chipnet.imaginary.cash:50004,wss://chipnet.c3-soft.com:64004") .split(",").map((s) => s.trim()).filter(Boolean), wsBind: process.env.BNS_WS_BIND || "127.0.0.1", wsPort: Number(process.env.BNS_WS_PORT || 50010), beacon: process.env.BNS_BEACON_SCRIPTHASH || "e1cdbba719436f49acb4bf245b44df875c6d6b405eda46a9d3e736d138102f3b", pollMs: Number(process.env.BNS_POLL_MS || 30000), }; const log = (...a) => console.log(new Date().toISOString(), ...a); // minimal newline-delimited electrum WS client (tolerates a missing trailing newline). function elClient(url) { return new Promise((resolve, reject) => { const ws = new WebSocket(url); let id = 0, buf = ""; const pending = new Map(); const t = setTimeout(() => { try { ws.close(); } catch {} reject(new Error("connect timeout " + url)); }, 15000); ws.on("open", () => { clearTimeout(t); resolve({ call(method, params = []) { return new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params }) + "\n"); setTimeout(() => { if (pending.has(i)) { pending.delete(i); rej(new Error("timeout " + method)); } }, 20000); }); }, close() { try { ws.close(); } catch {} }, }); }); ws.on("error", (e) => { clearTimeout(t); reject(e); }); const settle = (line) => { try { const m = JSON.parse(line); const p = pending.get(m.id); if (p) { pending.delete(m.id); m.error ? p.rej(new Error(JSON.stringify(m.error))) : p.res(m.result); } } catch {} }; ws.on("message", (d) => { buf += d.toString(); let nl; while ((nl = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1); if (line) settle(line); } const rest = buf.trim(); if (rest) { try { JSON.parse(rest); settle(rest); buf = ""; } catch {} } }); }); } async function connectAny(servers) { let lastErr; for (const url of servers) { try { const cl = await elClient(url); await cl.call("server.version", ["BNS-Relay/0.1", "1.4.3"]); return { cl, url }; } catch (e) { lastErr = e; log("upstream unavailable:", url, "-", e.message); } } throw lastErr || new Error("no upstream servers configured"); } // Pull the beacon history + any not-yet-cached txs from an upstream, into the index. async function sync(index, servers) { const { cl, url } = await connectAny(servers); try { const history = await cl.call("blockchain.scripthash.get_history", [CONFIG.beacon]); if (!Array.isArray(history)) throw new Error("bad get_history response"); let fetched = 0; for (const { tx_hash, height } of history) { index.history.set(tx_hash, height); if (!index.txCache.has(tx_hash)) { index.txCache.set(tx_hash, await cl.call("blockchain.transaction.get", [tx_hash, true])); fetched++; } } log(`synced via ${url}: ${index.history.size} beacon txs (${fetched} new)`); } finally { cl.close(); } } async function main() { log("electrum-sourced BNS indexer starting — upstreams:", CONFIG.servers.join(", ")); const index = new BeaconIndex(CONFIG.beacon); startElectrumWS(index, { bind: CONFIG.wsBind, port: CONFIG.wsPort }, null); // cache-only: beacon txs are all cached for (;;) { try { await sync(index, CONFIG.servers); } catch (e) { log("sync error (will retry):", e.message); } await new Promise((r) => setTimeout(r, CONFIG.pollMs)); } } if (import.meta.url === pathToFileURL(process.argv[1] || "").href) { main().catch((e) => { console.error("fatal:", e.message); process.exit(1); }); }