Zero dependencies, no account, no API. Every command takes --rpc, so it runs against a node you operate and never needs ours. That is the point: an exposure measurement you cannot re-run is an opinion with a logo on it. Ships with two self-checks meant to be run by you, not just by us. The second, proba-vocabular.mjs, exists because of a real defect found on 2026-08-16: the scanner emits RED / YELLOW / GREEN, two downstream files each wrote their own copy of that list, and both wrote AMBER, so a hybrid contract, exactly a client who has already started migrating, was reported as unmeasurable. Both sides were self-consistent, so nothing we had could see it. The vocabulary now has one exported source, and the test walks the whole path with a negative control that makes it able to fail. The same scanner returns RED about our own contracts. See VERIFY-US.md.
89 lines
3.2 KiB
JavaScript
89 lines
3.2 KiB
JavaScript
// 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 };
|
|
}
|
|
}
|