// rpc.js, minimal JSON-RPC client using Node's global fetch (Node 18+). // Zero dependencies. Used only for eth_getCode and eth_chainId in the scanner. let _id = 0; /** * Single JSON-RPC call. * @param {string} url RPC endpoint * @param {string} method * @param {Array} params * @param {number} timeoutMs */ export async function rpcCall(url, method, params = [], timeoutMs = 15000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++_id, method, params }), signal: controller.signal, }); if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); const body = await res.json(); if (body.error) throw new Error(`RPC error ${body.error.code}: ${body.error.message}`); return body.result; } finally { clearTimeout(timer); } } /** eth_getCode at latest. Returns 0x-prefixed hex ('0x' for an EOA). */ export async function getCode(url, address, timeoutMs) { return rpcCall(url, 'eth_getCode', [address, 'latest'], timeoutMs); } /** eth_chainId as a decimal number, or null on failure. */ export async function getChainId(url, timeoutMs) { try { const hex = await rpcCall(url, 'eth_chainId', [], timeoutMs); return parseInt(hex, 16); } catch { return null; } } // ── read-only getters used by the migration SDK ────────────────────────────── // All of these are READ methods (eth_call / eth_getBalance / eth_estimateGas / // eth_getTransactionCount / eth_gasPrice). None of them broadcast a transaction, // none of them can move funds. eth_estimateGas simulates against pending state // and returns a gas number; it does not submit anything. /** eth_getBalance at `block` (default latest). Returns a BigInt of wei. */ export async function getBalance(url, address, block = 'latest', timeoutMs) { const hex = await rpcCall(url, 'eth_getBalance', [address, block], timeoutMs); return BigInt(hex); } /** eth_call (read-only). `tx` = { to, data, from? }. Returns 0x-hex return data. */ export async function ethCall(url, tx, block = 'latest', timeoutMs) { return rpcCall(url, 'eth_call', [tx, block], timeoutMs); } /** eth_getTransactionCount at `block` (default pending) => account nonce (number). */ export async function getTransactionCount(url, address, block = 'pending', timeoutMs) { const hex = await rpcCall(url, 'eth_getTransactionCount', [address, block], timeoutMs); return parseInt(hex, 16); } /** eth_gasPrice => BigInt wei. */ export async function getGasPrice(url, timeoutMs) { const hex = await rpcCall(url, 'eth_gasPrice', [], timeoutMs); return BigInt(hex); } /** * eth_estimateGas for an unsigned tx (read-only simulation, no broadcast). * Returns { gas } on success or { error } if the node reverts the simulation * (e.g. a token with no allowance yet). Never throws for a revert. */ export async function estimateGas(url, tx, timeoutMs) { try { const hex = await rpcCall(url, 'eth_estimateGas', [tx], timeoutMs); return { gas: parseInt(hex, 16) }; } catch (e) { return { error: e.message }; } }