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.
248 lines
13 KiB
JavaScript
248 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
// aere-pqc.js, the unified Aere Quantum Migration Toolkit CLI. One entry point
|
|
// wrapping the whole product: scan an account, simulate fleet migration cost,
|
|
// build an unsigned migration transaction sequence, and print a per-account
|
|
// quantum-readiness report.
|
|
//
|
|
// aere-pqc scan <address> --rpc <url>
|
|
// aere-pqc simulate --count 100 --scheme hybrid --auths 5
|
|
// aere-pqc build-migration --eoa 0x.. --falcon 0x09.. [--path direct|migrator]
|
|
// aere-pqc readiness <address> --rpc <url> [--tokens 0x..,0x..] [--scheme ..]
|
|
//
|
|
// Nothing in this CLI signs, deploys, or broadcasts. The only network calls are
|
|
// read-only JSON-RPC (eth_getCode/getBalance/call/estimateGas/gasPrice).
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
import { scanLive, scanBytecode, isValidAddress } from './lib/scanner.js';
|
|
import { simulateMigration, formatSimulation, schemeList } from './lib/simulate.js';
|
|
import {
|
|
buildMigrationTransactions, enumerateAssets, estimateMigrationGas, predictTargetAddress,
|
|
} from './lib/migrator.js';
|
|
import { buildReadinessReport, formatReadinessReport, readinessSchemeList } from './lib/readiness.js';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DEFAULT_RPC = 'https://rpc.aere.network';
|
|
|
|
const isTTY = process.stdout.isTTY;
|
|
const C = isTTY
|
|
? { b: (s) => `\x1b[1m${s}\x1b[0m`, dim: (s) => `\x1b[2m${s}\x1b[0m`, g: (s) => `\x1b[32m${s}\x1b[0m`, y: (s) => `\x1b[33m${s}\x1b[0m` }
|
|
: { b: (s) => s, dim: (s) => s, g: (s) => s, y: (s) => s };
|
|
|
|
function loadSampleKey() {
|
|
const p = join(__dirname, '..', 'contracts', 'deployments', 'pqc-account.json');
|
|
return JSON.parse(readFileSync(p, 'utf8')).sampleAccount.falconPubKey;
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const a = { _: [] };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const t = argv[i];
|
|
if (t.startsWith('--')) {
|
|
const key = t.slice(2);
|
|
const next = argv[i + 1];
|
|
if (next === undefined || next.startsWith('--')) { a[key] = true; }
|
|
else { a[key] = next; i++; }
|
|
} else a._.push(t);
|
|
}
|
|
return a;
|
|
}
|
|
|
|
function topUsage(code = 0) {
|
|
console.log(`${C.b('Aere Quantum Migration Toolkit')} (chain 2800)
|
|
|
|
Commands
|
|
${C.b('scan')} <address> --rpc <url> classify quantum exposure (RED/YELLOW/GREEN)
|
|
${C.b('simulate')} --count <n> [--scheme <s>] fleet migration-cost projection
|
|
${C.b('build-migration')} --eoa 0x.. --falcon 0x09.. build UNSIGNED migration txs
|
|
${C.b('readiness')} <address> --rpc <url> one-account readiness + cost report
|
|
|
|
Global
|
|
--rpc <url> JSON-RPC endpoint (default ${DEFAULT_RPC})
|
|
--json machine-readable output
|
|
--help this help; or "<command> --help"
|
|
|
|
Scope: this classifies and CONSTRUCTS/SIMULATES migration of ACCOUNT/APPLICATION
|
|
quantum exposure. It never signs, deploys, or moves funds, and it does NOT make
|
|
Aere consensus post-quantum (2800 seals with classical secp256k1 QBFT).`);
|
|
process.exit(code);
|
|
}
|
|
|
|
// ── scan ─────────────────────────────────────────────────────────────────────
|
|
async function cmdScan(args) {
|
|
const addr = args._[0];
|
|
const rpc = args.rpc === true ? DEFAULT_RPC : (args.rpc || DEFAULT_RPC);
|
|
let report;
|
|
if (args.fixture) {
|
|
const path = join(__dirname, 'fixtures', args.fixture + '.hex');
|
|
report = scanBytecode(`fixture:${args.fixture}`, readFileSync(path, 'utf8').trim());
|
|
} else if (args.bytecode) {
|
|
report = scanBytecode(addr || '(provided bytecode)', args.bytecode);
|
|
} else if (addr && isValidAddress(addr)) {
|
|
report = await scanLive(addr, rpc);
|
|
} else {
|
|
console.error('scan needs <address> --rpc <url>, or --fixture <name>, or --bytecode 0x..');
|
|
process.exit(1);
|
|
}
|
|
if (args.json) { console.log(JSON.stringify(report, (_k, v) => (typeof v === 'bigint' ? '0x' + v.toString(16) : v), 2)); return; }
|
|
const badge = report.readiness === 'GREEN' ? C.g(' GREEN ') : report.readiness === 'YELLOW' ? C.y(' YELLOW ') : ' RED ';
|
|
console.log(`\n[${badge}] ${C.b(report.address)}`);
|
|
console.log(` kind: ${report.kind}${report.live ? ` chainId: ${report.live.chainId} code: ${report.live.codeBytes} bytes` : ''}`);
|
|
if (report.reason) console.log(` ${C.dim(report.reason)}`);
|
|
if (report.migration) console.log(` ${C.b('migration')}: ${report.migration}`);
|
|
console.log('');
|
|
}
|
|
|
|
// ── simulate ─────────────────────────────────────────────────────────────────
|
|
function cmdSimulate(args) {
|
|
if (args.schemes) { console.log(schemeList().join('\n')); return; }
|
|
let accounts;
|
|
if (args.accounts) {
|
|
accounts = JSON.parse(readFileSync(args.accounts, 'utf8'));
|
|
} else {
|
|
const count = parseInt(args.count, 10) || 1;
|
|
accounts = Array.from({ length: count }, () => ({
|
|
scheme: args.scheme || 'falcon512',
|
|
authsPerAccount: args.auths ? parseInt(args.auths, 10) : 1,
|
|
deployAccount: !args['no-deploy'],
|
|
}));
|
|
}
|
|
const sim = simulateMigration(accounts, { gweiPrice: args.gwei ? parseFloat(args.gwei) : undefined });
|
|
if (args.json) { console.log(JSON.stringify(sim, null, 2)); return; }
|
|
console.log('\n' + formatSimulation(sim) + '\n');
|
|
}
|
|
|
|
// ── build-migration ──────────────────────────────────────────────────────────
|
|
async function cmdBuildMigration(args) {
|
|
const falcon = args.falcon && args.falcon !== true ? args.falcon : loadSampleKey();
|
|
const usingSample = !(args.falcon && args.falcon !== true);
|
|
const eoa = args.eoa && args.eoa !== true ? args.eoa : '0x000000000000000000000000000000000000dEaD';
|
|
const usingDemoEoa = !(args.eoa && args.eoa !== true);
|
|
const salt = args.salt ? parseInt(args.salt, 10) : 0;
|
|
const path = args.path && args.path !== true ? args.path : 'direct';
|
|
const rpc = args.rpc === true ? DEFAULT_RPC : args.rpc;
|
|
|
|
// asset selection: explicit --tokens list (with optional --amounts) or, if
|
|
// --rpc is given, enumerate real balances read-only.
|
|
let tokens = [];
|
|
let nativeWei = 0n;
|
|
const tokenAddrs = args.tokens && args.tokens !== true ? String(args.tokens).split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
let enumerated = null;
|
|
// When moving native AERE from the SAME EOA that pays gas, you cannot move the
|
|
// ENTIRE balance: the tx itself needs gas. Reserve a buffer (default 0.01 AERE,
|
|
// override with --native-reserve <wei>) so the native move can actually settle.
|
|
const nativeReserve = args['native-reserve'] && args['native-reserve'] !== true
|
|
? BigInt(args['native-reserve']) : 10000000000000000n; // 1e16 wei = 0.01 AERE
|
|
let reservedNote = null;
|
|
const explicitNative = args.native && args.native !== true ? BigInt(args.native) : null;
|
|
|
|
// native amount: explicit --native wins everywhere; else --no-native = 0; else
|
|
// (only with live enumeration) auto-drain balance minus a gas reserve.
|
|
if (explicitNative != null) nativeWei = explicitNative;
|
|
else if (args['no-native']) nativeWei = 0n;
|
|
|
|
if (rpc && !usingDemoEoa) {
|
|
enumerated = await enumerateAssets({ rpcUrl: rpc, owner: eoa, tokens: tokenAddrs });
|
|
const full = BigInt(enumerated.native.balanceWei);
|
|
if (explicitNative != null) {
|
|
reservedNote = `native move = explicit --native ${nativeWei} wei (current balance ${full} wei).`;
|
|
} else if (!args['no-native']) {
|
|
if (path === 'direct') {
|
|
nativeWei = full > nativeReserve ? full - nativeReserve : 0n;
|
|
if (full > 0n) reservedNote = `native move = balance ${full} minus a ${nativeReserve} wei gas reserve = ${nativeWei} wei. NOTE: draining an ACTIVE account races its own spending; prefer --native <wei> for a fixed amount.`;
|
|
} else {
|
|
nativeWei = full; // migrator forwards msg.value; the EOA still pays gas, so leave headroom beyond this
|
|
if (full > 0n) reservedNote = `native move = full balance ${full} wei; ensure the migrate tx has gas headroom beyond this (migrator forwards msg.value).`;
|
|
}
|
|
}
|
|
tokens = enumerated.tokens.filter((t) => t.needsMove).map((t) => ({ token: t.token, amountWei: t.balance }));
|
|
} else if (tokenAddrs.length && args.amounts && args.amounts !== true) {
|
|
const amts = String(args.amounts).split(',').map((s) => s.trim());
|
|
tokens = tokenAddrs.map((t, i) => ({ token: t, amountWei: BigInt(amts[i] || '0') }));
|
|
} else {
|
|
tokens = tokenAddrs.map((t) => ({ token: t, amountWei: 1n })); // placeholder amount; user sets real value
|
|
}
|
|
|
|
const plan = buildMigrationTransactions({
|
|
path, eoa, falconPubKey: falcon, salt,
|
|
tokens, nativeWei,
|
|
deployAccount: !args['no-deploy'],
|
|
migrator: args.migrator && args.migrator !== true ? args.migrator : undefined,
|
|
bindFalcon: !args['no-bind'],
|
|
});
|
|
|
|
if (rpc && args.estimate) await estimateMigrationGas(plan, rpc);
|
|
|
|
if (args.json) { console.log(JSON.stringify(plan, (_k, v) => (typeof v === 'bigint' ? v.toString() : v), 2)); return; }
|
|
|
|
console.log(`\n${C.b('Aere post-quantum migration')} (path: ${plan.path})`);
|
|
if (usingSample) console.log(C.dim(" (demo: target derived from the live sample account's real Falcon-512 key)"));
|
|
if (usingDemoEoa) console.log(C.dim(' (demo EOA 0x..dEaD; pass --eoa 0xYourAccount for a real build)'));
|
|
console.log(` from EOA: ${plan.eoa}`);
|
|
console.log(` factory (live): ${plan.factory}`);
|
|
console.log(` target account: ${C.b(plan.target)}`);
|
|
if (enumerated) console.log(` enumerated: ${enumerated.movable} movable asset(s) via ${rpc}`);
|
|
if (reservedNote) console.log(` ${C.dim(reservedNote)}`);
|
|
console.log('');
|
|
console.log(` ${C.b('Unsigned transactions')} (${plan.transactions.length}). Sign each in your own wallet, in order:`);
|
|
for (const item of plan.transactions) {
|
|
console.log(`\n Step ${item.step}. [${item.kind}] ${C.dim('(signed: ' + item.signed + ')')}`);
|
|
console.log(` ${item.description}`);
|
|
console.log(` to: ${item.tx.to}`);
|
|
console.log(` value: ${item.tx.value}`);
|
|
console.log(` data: ${item.tx.data.length > 50 ? item.tx.data.slice(0, 50) + '..' : item.tx.data} (${(item.tx.data.length - 2) / 2} bytes)`);
|
|
if (item.gasEstimate != null) console.log(` gas (live estimate): ${item.gasEstimate.toLocaleString()}`);
|
|
if (item.gasEstimateError) console.log(` gas: ${C.dim('estimate n/a (' + item.gasEstimateError + ')')}`);
|
|
for (const w of item.warnings || []) console.log(` ${C.y('warn')}: ${w}`);
|
|
}
|
|
if (plan.warnings.length) {
|
|
console.log(`\n ${C.b('Warnings')}:`);
|
|
for (const w of plan.warnings) console.log(` - ${w}`);
|
|
}
|
|
console.log(`\n ${C.dim(plan.scope)}\n`);
|
|
}
|
|
|
|
// ── readiness ────────────────────────────────────────────────────────────────
|
|
async function cmdReadiness(args) {
|
|
const addr = args._[0];
|
|
if (!addr || !isValidAddress(addr)) { console.error('readiness needs <address>'); process.exit(1); }
|
|
const rpc = args.rpc === true ? DEFAULT_RPC : (args.rpc || DEFAULT_RPC);
|
|
const scan = await scanLive(addr, rpc);
|
|
const tokenAddrs = args.tokens && args.tokens !== true ? String(args.tokens).split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
const assets = await enumerateAssets({ rpcUrl: rpc, owner: addr, tokens: tokenAddrs });
|
|
const report = buildReadinessReport({
|
|
scan, assets,
|
|
scheme: args.scheme && args.scheme !== true ? args.scheme : 'falcon512_solidity',
|
|
path: args.path && args.path !== true ? args.path : 'direct',
|
|
projectedAuths: args.auths ? parseInt(args.auths, 10) : 1,
|
|
gwei: args.gwei ? parseFloat(args.gwei) : undefined,
|
|
});
|
|
if (args.json) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
console.log(formatReadinessReport(report));
|
|
}
|
|
|
|
async function main() {
|
|
const argv = process.argv.slice(2);
|
|
const cmd = argv[0];
|
|
if (!cmd || cmd === '--help' || cmd === '-h') topUsage(0);
|
|
const args = parseArgs(argv.slice(1));
|
|
if (args.help) {
|
|
console.log(`See: aere-pqc ${cmd} ... (schemes: sim=${schemeList().join(',')} ; readiness=${readinessSchemeList().join(',')})`);
|
|
return;
|
|
}
|
|
try {
|
|
if (cmd === 'scan') await cmdScan(args);
|
|
else if (cmd === 'simulate') cmdSimulate(args);
|
|
else if (cmd === 'build-migration') await cmdBuildMigration(args);
|
|
else if (cmd === 'readiness') await cmdReadiness(args);
|
|
else { console.error(`unknown command: ${cmd}`); topUsage(1); }
|
|
} catch (e) {
|
|
console.error(`error: ${e.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|