Setup 5d15508bba929f1f074c052ac933863eadf6eb8e56984ebd5a1af75e80626643 Portable a5d346b97f5a13d85fa3bd301a72075ddb82fe636d7b1a51840ffd5a16d879f4 Bundled since 0.3.27: 32d4b75 - Aegis (bchwallet) gains its own update card in Settings > General beside Ariadne. Check for updates hits the same signed OTA endpoint the boot timer uses; Restart to apply appears when a signed newer version is staged. Uses the existing addons-check-updates + a new app-restart IPC. New Aegis versions ship without a Theseus release. 32d4b75 (same commit) - DevTools (F12 / Ctrl+Shift+I) opens docked to the right of the tab (mode: 'right') instead of a detached window. Matches stock Chrome. Users who prefer detached can drag out via the DevTools own toolbar. b71c925 - Search-engine favicons in Settings > Search now use Google's /s2/favicons service — DuckDuckGo's ip3 source returned 404 for enough hosts (Brave, Bing, Yandex, etc.) that half the list was falling through to the emoji placeholder. Deployed. Verified LIVE 0.3.28.
131 lines
5.2 KiB
JavaScript
131 lines
5.2 KiB
JavaScript
// Electrum (Fulcrum) JSON-RPC over WebSocket for the wallet. One live
|
|
// connection at a time, chosen by walking the server list in order; the
|
|
// caller gets a stable `call()` that reconnects transparently on the next
|
|
// request after a drop. Notifications (headers / scripthash subscriptions)
|
|
// fan out to `onNotify`.
|
|
module.exports = function makeElectrum({ WebSocket, log = () => {} }) {
|
|
const CALL_TIMEOUT_MS = 20000;
|
|
|
|
class Connection {
|
|
constructor(url) {
|
|
this.url = url;
|
|
this.id = 0;
|
|
this.pending = new Map();
|
|
this.buf = "";
|
|
this.closed = false;
|
|
this.onNotify = null;
|
|
this.onClose = null;
|
|
}
|
|
connect() {
|
|
return new Promise((resolve, reject) => {
|
|
const ws = new WebSocket(this.url);
|
|
this.ws = ws;
|
|
const fail = (e) => { if (!this.closed) { this.closed = true; reject(e instanceof Error ? e : new Error("electrum ws error: " + this.url)); } };
|
|
ws.on("open", async () => {
|
|
try { await this.call("server.version", ["theseus-bchwallet", "1.4"]); resolve(this); }
|
|
catch (e) { fail(e); this.close(); }
|
|
});
|
|
ws.on("error", fail);
|
|
ws.on("message", (d) => this._onData(String(d)));
|
|
ws.on("close", () => {
|
|
this.closed = true;
|
|
for (const p of this.pending.values()) p.reject(new Error("electrum connection closed"));
|
|
this.pending.clear();
|
|
if (this.onClose) this.onClose();
|
|
});
|
|
});
|
|
}
|
|
_onData(chunk) {
|
|
this.buf += chunk;
|
|
let nl;
|
|
while ((nl = this.buf.indexOf("\n")) >= 0) {
|
|
const line = this.buf.slice(0, nl).trim();
|
|
this.buf = this.buf.slice(nl + 1);
|
|
if (line) this._handleLine(line);
|
|
}
|
|
const rest = this.buf.trim();
|
|
if (rest) { try { JSON.parse(rest); this._handleLine(rest); this.buf = ""; } catch {} }
|
|
}
|
|
_handleLine(line) {
|
|
let msg;
|
|
try { msg = JSON.parse(line); } catch { return; }
|
|
if (msg.id != null && this.pending.has(msg.id)) {
|
|
const p = this.pending.get(msg.id);
|
|
this.pending.delete(msg.id);
|
|
clearTimeout(p.timer);
|
|
if (msg.error) p.reject(new Error(typeof msg.error === "object" ? (msg.error.message || JSON.stringify(msg.error)) : String(msg.error)));
|
|
else p.resolve(msg.result);
|
|
} else if (msg.method && this.onNotify) {
|
|
this.onNotify(msg.method, msg.params || []);
|
|
}
|
|
}
|
|
call(method, params = []) {
|
|
if (this.closed) return Promise.reject(new Error("electrum connection closed"));
|
|
const id = ++this.id;
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(`electrum timeout: ${method}`)); }
|
|
}, CALL_TIMEOUT_MS);
|
|
this.pending.set(id, { resolve, reject, timer });
|
|
try { this.ws.send(JSON.stringify({ id, method, params }) + "\n"); }
|
|
catch (e) { clearTimeout(timer); this.pending.delete(id); reject(e); }
|
|
});
|
|
}
|
|
close() { this.closed = true; try { this.ws.close(); } catch {} }
|
|
}
|
|
|
|
class Client {
|
|
constructor(servers) {
|
|
this.servers = servers.slice();
|
|
this.conn = null;
|
|
this.connecting = null;
|
|
this.subscriptions = new Map(); // method+key -> params (replayed on reconnect)
|
|
this.onNotify = null;
|
|
this.onServer = null; // (url|null) connection state for the UI
|
|
}
|
|
setServers(servers) {
|
|
this.servers = servers.slice();
|
|
this.disconnect();
|
|
}
|
|
get url() { return this.conn && !this.conn.closed ? this.conn.url : null; }
|
|
async _ensure() {
|
|
if (this.conn && !this.conn.closed) return this.conn;
|
|
if (this.connecting) return this.connecting;
|
|
this.connecting = (async () => {
|
|
let lastErr;
|
|
for (const url of this.servers) {
|
|
try {
|
|
const c = await new Connection(url).connect();
|
|
c.onNotify = (m, p) => { if (this.onNotify) this.onNotify(m, p); };
|
|
c.onClose = () => { if (this.conn === c) { this.conn = null; if (this.onServer) this.onServer(null); } };
|
|
this.conn = c;
|
|
log("connected", url);
|
|
if (this.onServer) this.onServer(url);
|
|
// Re-arm subscriptions so a reconnect keeps the live feed.
|
|
for (const params of this.subscriptions.values()) c.call(params[0], params[1]).catch(() => {});
|
|
return c;
|
|
} catch (e) { lastErr = e; log("failed", url, e?.message); }
|
|
}
|
|
throw lastErr || new Error("no electrum server reachable");
|
|
})();
|
|
try { return await this.connecting; }
|
|
finally { this.connecting = null; }
|
|
}
|
|
async call(method, params = []) {
|
|
const c = await this._ensure();
|
|
return c.call(method, params);
|
|
}
|
|
// Remember a subscription so it survives reconnects.
|
|
async subscribe(method, params = []) {
|
|
this.subscriptions.set(method + ":" + JSON.stringify(params), [method, params]);
|
|
return this.call(method, params);
|
|
}
|
|
clearSubscriptions() { this.subscriptions.clear(); }
|
|
disconnect() {
|
|
if (this.conn) { const c = this.conn; this.conn = null; c.close(); }
|
|
if (this.onServer) this.onServer(null);
|
|
}
|
|
}
|
|
|
|
return { Client };
|
|
};
|