// wRPC JSON client for igneumd. Node 22, no dependencies. Method names are the lowerCamelCase of the node's // RpcApiOps (getBlockDagInfo, getFinalityCheckpoints, getFinalityWeights, submitFinalityVote, ...). export class Rpc { constructor(url, { timeoutMs = 10_000 } = {}) { this.url = url; this.id = 0; this.pending = new Map(); this.ws = null; this.open = false; this.timeoutMs = timeoutMs; this.onNotification = () => { }; } connect() { return new Promise((resolve) => { const ws = new WebSocket(this.url); this.ws = ws; ws.onopen = () => { this.open = true; resolve(true); }; ws.onmessage = (e) => { let m; try { m = JSON.parse(e.data); } catch { return; } if (m.id !== undefined && m.id !== null && this.pending.has(m.id)) { const p = this.pending.get(m.id); this.pending.delete(m.id); m.error ? p.reject(new Error(typeof m.error === 'string' ? m.error : (m.error.message || JSON.stringify(m.error)))) : p.resolve(m.params); } else if (m.method) this.onNotification(m.method, m.params); }; ws.onerror = () => { if (!this.open) resolve(false); }; ws.onclose = () => { const wasOpen = this.open; this.open = false; for (const p of this.pending.values()) p.reject(new Error('rpc closed')); this.pending.clear(); if (!wasOpen) resolve(false); }; setTimeout(() => { if (!this.open) { try { ws.close(); } catch { } resolve(false); } }, 3000); }); } close() { try { this.ws && this.ws.close(); } catch { } } call(method, params = {}, timeoutMs = this.timeoutMs) { return new Promise((resolve, reject) => { if (!this.open) return reject(new Error('rpc not connected')); const id = ++this.id; this.pending.set(id, { resolve, reject }); this.ws.send(JSON.stringify({ id, method, params })); setTimeout(() => { if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(`${method} timed out`)); } }, timeoutMs); }); } } export async function connectRpc(url, { attempts = 60, waitMs = 500 } = {}) { for (let i = 0; i < attempts; i++) { const rpc = new Rpc(url); if (await rpc.connect()) { try { await rpc.call('getInfo'); return rpc; } catch { rpc.close(); } } await new Promise(r => setTimeout(r, waitMs)); } throw new Error(`could not reach ${url}`); } // Convenience readers (camelCase fields from the node's RPC model). export async function checkpoints(rpc, last = 40) { return rpc.call('getFinalityCheckpoints', { last }); } export async function weights(rpc) { return rpc.call('getFinalityWeights', {}); }