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