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.
108 lines
3.7 KiB
JavaScript
108 lines
3.7 KiB
JavaScript
// abi.js, a small, zero-dependency ABI encoder covering exactly the type set the
|
|
// migration SDK needs to build AereAccountMigrator and ERC-20 calldata:
|
|
//
|
|
// uint256, address, bool, bytes, address[], uint256[]
|
|
//
|
|
// It implements the standard Solidity head/tail layout (dynamic parameters get a
|
|
// 32-byte offset in the head and their bytes in the tail). It is validated
|
|
// byte-for-byte against ethers v6 in selftest.js, so the calldata this toolkit
|
|
// hands a user to sign is provably identical to what a full ABI library emits.
|
|
//
|
|
// This file ENCODES calldata only. It never signs, deploys, or broadcasts.
|
|
|
|
import { keccak256 } from './keccak.js';
|
|
|
|
function stripHex(h) {
|
|
return h.startsWith('0x') || h.startsWith('0X') ? h.slice(2) : h;
|
|
}
|
|
|
|
function uint256Hex(value) {
|
|
const v = BigInt(value);
|
|
if (v < 0n) throw new Error('uint256 cannot be negative');
|
|
const hex = v.toString(16);
|
|
if (hex.length > 64) throw new Error('uint256 overflow');
|
|
return hex.padStart(64, '0');
|
|
}
|
|
|
|
function addressHex(addr) {
|
|
const a = stripHex(addr).toLowerCase();
|
|
if (a.length !== 40 || /[^0-9a-f]/.test(a)) throw new Error(`invalid address: ${addr}`);
|
|
return a.padStart(64, '0');
|
|
}
|
|
|
|
function boolHex(b) {
|
|
return (b ? 1n : 0n).toString(16).padStart(64, '0');
|
|
}
|
|
|
|
function isDynamic(type) {
|
|
return type === 'bytes' || type.endsWith('[]');
|
|
}
|
|
|
|
// Encode a single dynamic value to hex (no 0x), including its own length word.
|
|
function encodeDynamic(type, value) {
|
|
if (type === 'bytes') {
|
|
const h = stripHex(value).toLowerCase();
|
|
if (h.length % 2 !== 0) throw new Error('bytes must be an even-length hex string');
|
|
const byteLen = h.length / 2;
|
|
const padded = h.padEnd(Math.ceil(h.length / 64) * 64, '0');
|
|
return uint256Hex(byteLen) + padded;
|
|
}
|
|
if (type === 'address[]') {
|
|
if (!Array.isArray(value)) throw new Error('address[] expects an array');
|
|
return uint256Hex(value.length) + value.map(addressHex).join('');
|
|
}
|
|
if (type === 'uint256[]') {
|
|
if (!Array.isArray(value)) throw new Error('uint256[] expects an array');
|
|
return uint256Hex(value.length) + value.map(uint256Hex).join('');
|
|
}
|
|
throw new Error(`unsupported dynamic type: ${type}`);
|
|
}
|
|
|
|
function encodeStatic(type, value) {
|
|
if (type === 'uint256') return uint256Hex(value);
|
|
if (type === 'address') return addressHex(value);
|
|
if (type === 'bool') return boolHex(value);
|
|
throw new Error(`unsupported static type: ${type}`);
|
|
}
|
|
|
|
/**
|
|
* ABI-encode a parameter tuple. Returns 0x-prefixed hex.
|
|
* @param {string[]} types
|
|
* @param {any[]} values
|
|
*/
|
|
export function encodeParameters(types, values) {
|
|
if (types.length !== values.length) throw new Error('types/values length mismatch');
|
|
const head = [];
|
|
const tail = [];
|
|
let tailOffset = types.length * 32; // bytes; every head slot is 32 bytes
|
|
for (let i = 0; i < types.length; i++) {
|
|
const t = types[i];
|
|
if (isDynamic(t)) {
|
|
head.push(uint256Hex(tailOffset));
|
|
const enc = encodeDynamic(t, values[i]);
|
|
tail.push(enc);
|
|
tailOffset += enc.length / 2;
|
|
} else {
|
|
head.push(encodeStatic(t, values[i]));
|
|
}
|
|
}
|
|
return '0x' + head.join('') + tail.join('');
|
|
}
|
|
|
|
/** 4-byte selector (0x + 8 hex chars) for a canonical function signature. */
|
|
export function functionSelector(signature) {
|
|
const h = keccak256(new TextEncoder().encode(signature));
|
|
let s = '0x';
|
|
for (let i = 0; i < 4; i++) s += h[i].toString(16).padStart(2, '0');
|
|
return s;
|
|
}
|
|
|
|
/**
|
|
* Build full calldata: selector(signature) ++ encodeParameters(types, values).
|
|
* `signature` must be the canonical form, e.g. "migrate(address,address[],uint256[],bool)".
|
|
* @returns {string} 0x-prefixed calldata
|
|
*/
|
|
export function encodeFunctionData(signature, types, values) {
|
|
return functionSelector(signature) + encodeParameters(types, values).slice(2);
|
|
}
|