pqc-migration-toolkit/lib/rpc.js
Aere Network de9a79298c Aere PQC migration toolkit: measure an address quantum exposure from its bytecode
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.
2026-08-17 10:31:31 +03:00

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 };
}
}