#!/usr/bin/env node // scan.js, Aere PQC-readiness scanner CLI. // // node scan.js
--rpc live scan over JSON-RPC [MEASURE] // node scan.js
--bytecode 0x.. offline scan of given code // node scan.js --fixture offline scan of a bundled fixture // node scan.js --list-fixtures list bundled fixtures // // Reports RED (classical-only) / YELLOW (hybrid) / GREEN (PQC-capable) for an // EVM address on Aere (chain 2800) or any EVM chain reachable by RPC. import { readFileSync, readdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { scanLive, scanBytecode, isValidAddress } from './lib/scanner.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const FIXTURE_DIR = join(__dirname, 'fixtures'); const C = process.stdout.isTTY ? { red: (s) => `\x1b[31m${s}\x1b[0m`, yellow: (s) => `\x1b[33m${s}\x1b[0m`, green: (s) => `\x1b[32m${s}\x1b[0m`, dim: (s) => `\x1b[2m${s}\x1b[0m`, b: (s) => `\x1b[1m${s}\x1b[0m` } : { red: (s) => s, yellow: (s) => s, green: (s) => s, dim: (s) => s, b: (s) => s }; function usage(code = 0) { console.log(`Aere PQC-readiness scanner Usage node scan.js
--rpc live scan over JSON-RPC [MEASURE] node scan.js
--bytecode 0x.. offline scan of given runtime code node scan.js --fixture offline scan of a bundled fixture node scan.js --list-fixtures list bundled fixtures node scan.js
--rpc --json machine-readable output Readiness RED classical-only (EOA, or a contract with no PQC precompile usage) YELLOW hybrid (uses both a classical primitive and the PQC precompile band) GREEN PQC-capable (uses Aere's live native PQC precompiles 0x0AE1..0x0AE5) Scope: this classifies ACCOUNT / APPLICATION quantum exposure. It does not, and cannot, make Aere consensus post-quantum (chain 2800 seals with secp256k1 QBFT).`); process.exit(code); } function parseArgs(argv) { const a = { _: [] }; for (let i = 0; i < argv.length; i++) { const t = argv[i]; if (t === '--rpc') a.rpc = argv[++i]; else if (t === '--bytecode') a.bytecode = argv[++i]; else if (t === '--fixture') a.fixture = argv[++i]; else if (t === '--list-fixtures') a.listFixtures = true; else if (t === '--json') a.json = true; else if (t === '--help' || t === '-h') a.help = true; else a._.push(t); } return a; } function listFixtures() { return readdirSync(FIXTURE_DIR).filter((f) => f.endsWith('.hex')).map((f) => f.replace(/\.hex$/, '')); } function badge(readiness) { if (readiness === 'GREEN') return C.green(C.b(' GREEN ')); if (readiness === 'YELLOW') return C.yellow(C.b(' YELLOW ')); return C.red(C.b(' RED ')); } function printReport(r) { console.log(''); console.log(`${badge(r.readiness)} ${C.b(r.address)}`); console.log(` kind: ${r.kind}${r.live ? ` chainId: ${r.live.chainId ?? 'unknown'} code: ${r.live.codeBytes} bytes` : ''}`); if (r.kind === 'contract') console.log(` bytecode: ${r.bytecodeLength} bytes`); if (r.reason) console.log(` ${C.dim(r.reason)}`); const s = r.signals || {}; const pqc = s.pqcLivePrecompiles || []; if (pqc.length) { console.log(' post-quantum precompiles (live band 0x0AE1..0x0AE5):'); for (const p of pqc) console.log(` - ${p.address} ${p.name} ${C.dim('[' + p.confidence + (p.callProximate ? ', call-proximate' : '') + ']')}`); } if (s.p256) console.log(` classical: ${s.p256.address} ${s.p256.name} ${C.dim('[' + s.p256.confidence + ']')}`); if (s.ecrecover) console.log(` classical: ${s.ecrecover.address} ${s.ecrecover.name} ${C.dim('[' + s.ecrecover.confidence + ']')}`); if ((s.pqcTestnetPrecompiles || []).length) { for (const p of s.pqcTestnetPrecompiles) console.log(` ${C.yellow('testnet-only')}: ${p.address} ${p.name}`); } if (!pqc.length && !s.p256 && !s.ecrecover) console.log(' no signature-verification precompile usage detected in bytecode.'); if (r.migration) console.log(` ${C.b('migration')}: ${r.migration}`); if ((r.flags || []).length) { console.log(' notes:'); for (const f of r.flags) console.log(` ${C.dim('*')} ${f}`); } console.log(''); } async function main() { const args = parseArgs(process.argv.slice(2)); if (args.help) usage(0); if (args.listFixtures) { console.log(listFixtures().join('\n')); return; } let report; if (args.fixture) { const path = join(FIXTURE_DIR, args.fixture + '.hex'); let code; try { code = readFileSync(path, 'utf8').trim(); } catch { console.error(`fixture not found: ${args.fixture}. Available: ${listFixtures().join(', ')}`); process.exit(1); } report = scanBytecode(`fixture:${args.fixture}`, code); } else if (args.bytecode) { const addr = args._[0] || '(provided bytecode)'; report = scanBytecode(addr, args.bytecode); } else if (args._[0]) { const addr = args._[0]; if (!isValidAddress(addr)) { console.error(`invalid address: ${addr}`); process.exit(1); } if (!args.rpc) { console.error('a live address scan requires --rpc . [MEASURE] Provide an RPC, or use --bytecode / --fixture for an offline scan.'); process.exit(1); } try { report = await scanLive(addr, args.rpc); } catch (e) { console.error(`[MEASURE] live scan failed against ${args.rpc}: ${e.message}`); console.error('The RPC path is the only part of this tool that needs network access; offline --bytecode / --fixture scans still work.'); process.exit(2); } } else { usage(1); } if (args.json) { console.log(JSON.stringify(report, (_k, v) => (typeof v === 'bigint' ? '0x' + v.toString(16) : v), 2)); } else { printReport(report); } } main().catch((e) => { console.error(e); process.exit(1); });