@adraffy/blocksmith
Version:
Minimal Ethereum Testing Framework for Foundry + ethers
1,720 lines (1,661 loc) • 55.9 kB
JavaScript
import { spawn } from 'node:child_process';
import { ethers } from 'ethers';
import { createWriteStream } from 'node:fs';
import { mkdtemp, realpath, rm, mkdir, writeFile, access, readdir, readFile } from 'node:fs/promises';
import { join, dirname, basename, relative, normalize, sep } from 'node:path';
import { tmpdir } from 'node:os';
import { inspect } from 'node:util';
import { Console } from 'node:console';
import EventEmitter from 'node:events';
function error_with(message, params, cause) {
let error;
if (cause) {
error = new Error(message, {cause});
if (!error.cause) error.cause = cause;
} else {
error = new Error(message);
}
return Object.assign(error, params);
}
function is_address(s) {
return typeof s === 'string' && /^0x[0-9a-f]{40}$/i.test(s);
}
function to_address(x) {
if (x) {
if (is_address(x)) return x;
if (is_address(x.target)) return x.target;
if (is_address(x.address)) return x.address;
}
}
// https://toml.io/en/v1.0.0
function encode(obj) {
let lines = [];
write(lines, obj, []);
return lines.join('\n');
}
function write(lines, obj, path) {
let after = [];
for (let [k, v] of Object.entries(obj)) {
if (v === null) continue;
if (is_basic(v)) {
lines.push(`${encode_key(k)} = ${format_value(v)}`);
} else if (Array.isArray(v)) {
if (v.every(is_basic)) {
lines.push(`${encode_key(k)} = [${v.map(format_value)}]`);
} else {
after.push([k, v]);
}
} else if (v?.constructor === Object) {
after.push([k, v]);
} else {
throw error_with(`invalid type: "${k}"`, undefined, {key: k, value: v})
}
}
for (let [k, v] of after) {
path.push(encode_key(k));
if (Array.isArray(v)) {
let header = `[[${path.join('.')}]]`;
for (let x of v) {
lines.push(header);
write(lines, x, path);
}
} else {
lines.push(`[${path.join('.')}]`);
write(lines, v, path);
}
path.pop();
}
}
function format_value(x) {
if (typeof x === 'number' && Number.isInteger(x) && x > 9223372036854775000e0) {
return '9223372036854775000'; // next smallest javascript integer below 2^63-1
}
return JSON.stringify(x);
}
function encode_key(x) {
return /^[a-z_][a-z0-9_]*$/i.test(x) ? x : JSON.stringify(x);
}
function is_basic(x) {
//if (x === null) return true;
switch (typeof x) {
case 'boolean':
case 'number':
case 'string': return true;
}
}
/*
console.log(encode({
"fruits": [
{
"name": "apple",
"physical": {
"color": "red",
"shape": "round"
},
"varieties": [
{ "name": "red delicious" },
{ "name": "granny smith" }
]
},
{
"name": "banana",
"varieties": [
{ "name": "plantain" }
]
}
]
}));
*/
// https://docs.soliditylang.org/en/latest/grammar.html#a4.SolidityLexer.Identifier
function on_newline(fn) {
let prior = '';
return buf => {
prior += buf.toString();
let v = prior.split('\n');
prior = v.pop();
v.forEach(x => fn(x));
};
}
function is_pathlike(x) {
return typeof x === 'string' || x instanceof URL;
}
function remove_sol_ext(s) {
return s.replace(/\.sol$/, '');
}
const CONFIG_NAME = 'foundry.toml';
function ansi(c, s) {
return `\x1B[${c}m${s}\u001b[0m`;
}
function strip_ansi(s) {
return s.replaceAll(/[\u001b][^m]+m/g, ''); //.split('\n');
}
const TAG_START = ansi('93', 'LAUNCH');
const TAG_DEPLOY = ansi('33', 'DEPLOY');
const TAG_TX = ansi('33', 'TX');
const TAG_EVENT = ansi('36', 'EVENT');
const TAG_CONSOLE = ansi('96', 'LOG');
const TAG_STOP = ansi('93', 'STOP');
const DEFAULT_WALLET = 'admin';
const DEFAULT_PROFILE = 'default';
const Symbol_foundry = Symbol('blocksmith');
const Symbol_name = Symbol('blocksmith.name');
const Symbol_makeErrors = Symbol('blocksmith.makeError');
function get_NAME() {
return this[Symbol_name];
}
function smol_addr(addr) {
return addr.slice(2, 10);
}
function is_exact_semver(version) {
return typeof version === 'string' && /^\d+\.\d+\.\d+$/.test(version);
}
function parse_cid(cid) {
let pos = cid.lastIndexOf(':');
let contract;
if (pos == -1) {
contract = remove_sol_ext(basename(cid));
} else {
contract = remove_sol_ext(cid.slice(pos + 1));
cid = cid.slice(0, pos);
}
let path = cid.split(sep).reverse();
return {contract, path};
}
class ContractMap {
constructor() {
this.map = new Map();
}
add(cid, value) {
let {contract, path} = parse_cid(cid);
let bucket = this.map.get(contract);
if (!bucket) {
bucket = [];
this.map.set(contract, bucket);
}
bucket.push({path, value});
}
find(cid) {
let {contract, path} = parse_cid(cid);
let bucket = this.map.get(contract);
if (bucket) {
let i = 0;
for (; bucket.length > 1 && i < path.length; i++) {
bucket = bucket.filter(x => x.path[i] === path[i]);
}
if (bucket.length == 1) {
let cid = i ? `${path.slice(0, i).reverse().join(sep)}:${contract}` : contract;
return [cid, bucket[0].value];
}
}
return [];
}
}
async function exec({cmd, args = [], env = {}, /*cwd,*/ json = true} = {}) {
// 20240905: bun bug
// https://github.com/oven-sh/bun/issues/13755
// this fix is absolute garbage
// idea#1: use chunks[0].length != 262144
// 20240905: doesn't work
// idea#2: assume json, check for leading curly: /^\s*{/
// if (process.isBun && stdout.length > 1 && stdout[0][0] !== 0x7B) {
// console.log('out of order', stdout.map(x => x.length));
// let chunk = stdout[0];
// stdout[0] = stdout[1];
// stdout[1] = chunk;
// }
// 20240905: just use file until theres a proper fix
// https://github.com/oven-sh/bun/issues/4798
// 20240914: had to revert this fix as it causes more bugs than it fixes
// https://github.com/oven-sh/bun/issues/13972
// 20240921: another attempt to fix this bun shit
// just yolo swap the buffers if it parses incorrectly
try {
let stdout = await new Promise((ful, rej) => {
let proc = spawn(cmd, args, {
env: {...process.env, ...env},
stdio: ['ignore', 'pipe', 'pipe'],
//cwd,
});
let stdout = [];
let stderr = [];
proc.stdout.on('data', chunk => stdout.push(chunk));
proc.stderr.on('data', chunk => stderr.push(chunk));
proc.on('close', code => {
if (code) {
let error = Buffer.concat(stderr).toString('utf8');
error = strip_ansi(error);
error = error.replaceAll(/^Error:/g, '');
error = error.trim();
// 20240916: put more info in message since bun errors are dogshit
rej(new Error(`${cmd}: ${error} (code=${code})`));
} else {
//ful(Buffer.concat(stdout));
ful(stdout);
}
});
});
if (!json) {
return Buffer.concat(stdout);
}
try {
const buf = Buffer.concat(stdout);
return JSON.parse(buf);
} catch (bug) {
if (stdout.length > 1) {
let v = stdout.slice();
v[0] = stdout[1];
v[1] = stdout[0];
return JSON.parse(Buffer.concat(v));
}
throw bug;
}
} catch (err) {
throw Object.assign(err, {cmd, args, env/*, cwd*/});
}
}
async function compile(sol, options = {}) {
let {
contract,
foundry,
optimize,
autoHeader = true,
solcVersion,
evmVersion,
viaIR
} = options;
if (Array.isArray(sol)) {
sol = sol.join('\n');
}
if (!contract) {
let v = [...sol.matchAll(/(contract|library|interface)\s([a-z$_][0-9a-z$_]*)/ig)];
if (v.length > 1) v = v.filter(x => x[1] !== 'interface');
if (v.length != 1) throw error_with('expected contract name', {sol, names: v.map(x => x[2])});
contract = v[0][2];
}
if (autoHeader) {
if (!/^\s*pragma\s+solidity/m.test(sol)) {
sol = `pragma solidity >=0.0.0;\n${sol}`;
}
if (!/^\s*\/\/\s*SPDX-License-Identifier:/m.test(sol)) {
sol = `// SPDX-License-Identifier: UNLICENSED\n${sol}`;
}
}
let root = await mkdtemp(join(await realpath(tmpdir()), 'blocksmith-'));
await rm(root, {recursive: true, force: true}); // better than --force
let src = join(root, 'src');
await mkdir(src, {recursive: true});
let file = join(src, `${contract}.sol`);
await writeFile(file, sol);
let forge = foundry ? foundry.forge : 'forge';
let args = [
'build',
'--format-json',
'--root', root,
'--no-cache',
];
let profile = DEFAULT_PROFILE;
let env = {FOUNDRY_PROFILE: profile};
let config;
if (foundry) {
config = JSON.parse(JSON.stringify(foundry.config)); // structuredClone?
config.src = 'src';
config.test = 'src';
config.libs = [];
let remappings = [
['@src', foundry.config.src], // this is nonstandard
['@test', foundry.config.test],
...config.remappings.map(s => s.split('='))
];
config.remappings = remappings.map(([a, b]) => {
let pos = a.indexOf(':');
if (pos >= 0) {
// support remapping contexts
a = join(foundry.root, a.slice(0, pos) + a.slice(pos));
}
return `${a}=${join(foundry.root, b)}`;
});
} else {
config = {};
}
// cant use --optimize, no way to turn it off
let config_file = join(root, CONFIG_NAME);
if (optimize !== undefined) {
if (optimize === true) optimize = 200;
if (!optimize) {
config.optimizer = false;
} else {
config.optimizer = true;
config.optimizer_runs = optimize;
}
}
config.extra_output = ['metadata'];
if (solcVersion) config.solc = solcVersion;
if (evmVersion) config.evm_version = evmVersion;
if (viaIR !== undefined) config.via_ir = !!viaIR;
await writeFile(config_file, encode({profile: {[profile]: config}}));
args.push('--config-path', config_file);
const buildInfo = {
started: new Date(),
root,
cmd: [forge, ...args],
profile,
mode: foundry ? 'shadow' : 'compile',
force: true
};
foundry?.emit('building', buildInfo);
let res = await exec({
cmd: forge,
args,
env,
//cwd: root,
});
let errors = filter_errors(res.errors);
if (errors.length) {
throw error_with('forge build', {sol, errors});
}
buildInfo.sources = Object.keys(res.sources);
foundry?.emit('built', buildInfo);
let info = res.contracts[file]?.[contract]?.[0];
let origin = `InlineCode{${root.slice(-6)}}`;
if (!info) {
for (let x of Object.values(res.contracts)) {
let c = x[contract];
if (c) {
info = c[0];
//origin = '@import';
break;
}
}
if (!info) {
throw error_with('expected contract', {sol, contracts: Object.keys(res.contracts), contract});
}
}
let {contract: {abi, evm, metadata}} = info;
abi = abi_from_solc_json(abi);
let bytecode = '0x' + evm.bytecode.object;
let links = extract_links(evm.bytecode.linkReferences);
let cid = `${file}:${contract}`;
metadata = JSON.parse(metadata);
let compiler = metadata.compiler.version;
return {type: 'code', abi, bytecode, contract, origin, links, sol, cid, root, compiler};
}
// should this be called Foundry?
class FoundryBase extends EventEmitter {
constructor() {
super();
}
static profile() {
return process.env.FOUNDRY_PROFILE ?? DEFAULT_PROFILE;
}
static async root(cwd) {
let dir = await realpath(cwd || process.cwd());
while (true) {
let file = join(dir, 'foundry.toml');
try {
await access(file);
return dir;
} catch {
}
let parent = dirname(dir);
if (parent === dir) throw error_with(`expected ${CONFIG_NAME}`, {cwd});
dir = parent;
}
}
static async load({root, profile, forge = 'forge', ...unknown} = {}) {
if (Object.keys(unknown).length) {
throw error_with('unknown options', unknown);
}
if (!root) root = await this.root();
//root = await realpath(root); // do i need this?
if (!profile) profile = this.profile();
let config;
try {
config = await exec({
cmd: forge,
args: ['config', '--root', root, '--json'],
env: {FOUNDRY_PROFILE: profile},
//cwd: root
});
} catch (err) {
throw error_with(`invalid ${CONFIG_NAME}`, {root, profile}, err);
}
return Object.assign(new this, {root, profile, config, forge});
}
async version() {
const buf = await exec({
cmd: this.forge,
args: ['--version'],
//cwd: this.root,
json: false
});
return buf.toString('utf8').trim();
}
async compiler(solcVersion) {
// https://book.getfoundry.sh/reference/config/solidity-compiler#solc_version
if (!is_exact_semver(solcVersion)) throw new TypeError('expected exact semver: x.y.z')
const {compiler} = await compile('contract C {}', {solcVersion, foundry: this});
return compiler;
}
async exportArtifacts(dir, {force = true, tests = false, scripts = false} = {}) {
let args = [
'build',
'--format-json',
'--root', this.root,
'--no-cache',
// this is gigadangerous if not relative
// forge will happily just delete your entire computer
// if you pass: "--out=/"
'--out', join(this.root, dir),
];
if (force) args.push('--force');
if (!tests) args.push('--skip', 'test');
if (!scripts) args.push('--skip', 'script');
//let res = await exec(this.forge, args, {FOUNDRY_PROFILE: this.profile}, this.procLog);
//return res.errors;
return args;
}
async build(force) {
if (!force && this.built) return this.built;
const {forge, root, profile} = this;
let args = ['build', '--format-json', '--root', root];
if (force) args.push('--force');
const buildInfo = {
started: new Date(),
root,
cmd: [forge, ...args],
force,
profile,
mode: 'project'
};
this.emit('building', buildInfo);
let res = await exec({
cmd: forge,
args,
env: {FOUNDRY_PROFILE: profile},
//cwd: root
});
let errors = filter_errors(res.errors);
if (errors.length) {
throw error_with('forge build', {errors});
}
buildInfo.sources = Object.keys(res.sources);
this.emit('built', buildInfo);
return this.built = {date: new Date()};
}
async artifacts() {
await this.build();
const {out} = this.config;
const files = Array.from(await readdir(out, {recursive: true}));
const artifacts = [];
await Promise.all(files.map(async frag => {
if (!frag.endsWith('.json')) return;
try {
const artifact = await this.fileArtifact({file: join(out, frag)});
artifacts.push(artifact);
} catch (err) {
}
}));
return artifacts;
}
async find({file, contract}) {
await this.build();
file = remove_sol_ext(file); // remove optional extension
contract ??= basename(file); // derive contract name from file name
file += '.sol'; // add extension
let tail = join(basename(file), `${contract}.json`);
let path = dirname(file);
while (true) {
try {
let out_file = join(this.root, this.config.out, path, tail);
await access(out_file);
return out_file;
} catch (err) {
let parent = dirname(path);
if (parent === path) throw error_with(`unknown contract: ${file}:${contract}`, {file, contract});
path = parent;
}
}
}
compile(sol, options = {}) {
return compile(sol, {...options, foundry: this});
}
// async resolveABI(arg0) {
// return (await this.resolveArtifact(arg0)).abi;
// }
async resolveArtifact(arg0) {
let {import: imported, bytecode, abi, sol, contract, ...rest} = artifact_from(arg0);
if (bytecode) { // bytecode + abi
contract ??= 'Unnamed';
abi = iface_from(abi ?? []);
return {type: 'bytecode', abi, bytecode, contract, origin: 'Bytecode', links: []};
}
if (imported) {
contract ??= remove_sol_ext(basename(imported));
const artifact = await compile(`import "${imported}";`, {
...rest,
contract,
foundry: this,
autoHeader: true
});
artifact.origin = imported;
return artifact;
}
if (sol) { // sol code + contract?
return compile(sol, {contract, foundry: this, ...rest});
} else {
return this.fileArtifact({contract, ...rest});
}
}
async fileArtifact(arg0) {
let {file} = arg0;
let json, root, type;
if (typeof file === 'object') { // inline artifact (this might be dumb)
json = file;
file = undefined;
type = 'artifact';
} else if (file.endsWith('.json')) { // file artifact
json = JSON.parse(await readFile(file));
type = 'artifact';
} else { // source file
file = await this.find(arg0);
json = JSON.parse(await readFile(file));
root = this.root;
type = 'file';
}
let [origin, contract] = Object.entries(json.metadata.settings.compilationTarget)[0]; // TODO: is this correct?
let cid = `${origin}:${contract}`;
let bytecode = json.bytecode.object;
let links = extract_links(json.bytecode.linkReferences);
let abi = abi_from_solc_json(json.abi);
let compiler = json.metadata.compiler.version;
return {type, abi, bytecode, contract, origin, file, links, cid, root, compiler};
}
linkBytecode(bytecode, links, libs) {
let map = new ContractMap();
for (let [cid, impl] of Object.entries(libs)) {
let address = to_address(impl);
if (!address) throw error_with(`unable to determine library address: ${file}`, {file, impl});
map.add(cid, address);
}
let linkedLibs = {};
let linked = links.map(link => {
let cid = `${link.file}:${link.contract}`;
let [prefix, address] = map.find(cid);
if (!prefix) throw error_with(`unlinked external library: ${cid}`, link);
for (let offset of link.offsets) {
offset = (1 + offset) << 1;
bytecode = bytecode.slice(0, offset) + address.slice(2) + bytecode.slice(offset + 40);
}
linkedLibs[prefix] = address;
return {...link, cid, address};
});
bytecode = ethers.getBytes(bytecode);
return {bytecode, linked, linkedLibs};
}
tomlConfig() {
return encode({profile: {[this.profile]: this.config}});
}
// async deployArtifact() {
// // create server?
// // create static html?
// }
}
function has_key(x, key) {
return typeof x === 'object' && x !== null && key in x;
}
let _etherscanChains;
async function etherscanChains() {
return _etherscanChains ??= (async () => {
try {
const res = await fetch('https://api.etherscan.io/v2/chainlist');
if (!res.ok) throw new Error('etherscan: chainlist');
const {result} = await res.json();
return new Map(result.map(x => [BigInt(x.chainid), x.blockexplorer]));
} catch (err) {
_etherscanChains = undefined;
throw err;
}
})();
}
// let _chainlist;
// async function chainlist() {
// return _chainlist ??= (async () => {
// try {
// const res = await fetch('https://chainid.network/chains_mini.json');
// const result = await res.json();
// return new Map(result.map(x => [
// BigInt(x.chainId),
// {
// name: x.name,
// rpcs: x.rpc.filter(x => x.startsWith('https:') && !x.includes('{')),
// isETH: x.nativeCurrency.symbol === 'ETH' && x.nativeCurrency.decimals === 18,
// },
// ].filter(x => x[1].rpcs.length)));
// } catch (err) {
// _chainlist = undefined;
// throw err;
// }
// });
// }
// TODO: fix this
const PROVIDERS = {
mainnet: 'https://rpc.ankr.com/eth',
sepolia: 'https://rpc.ankr.com/eth_sepolia',
base: 'https://mainnet.base.org',
op: 'https://mainnet.optimism.io',
arb1: 'https://arb1.arbitrum.io/rpc',
linea: 'https://rpc.linea.build',
polygon: 'https://polygon-rpc.com',
};
class FoundryDeployer extends FoundryBase {
static etherscanChains = etherscanChains;
static async load({
provider,
privateKey,
gasToken = 'ETH',
infoLog = true,
...rest
} = {}) {
if (typeof provider === 'string') {
provider = new ethers.JsonRpcProvider(PROVIDERS[provider] || provider, null, {staticNetwork: true});
}
if (!infoLog) infoLog = undefined;
if (infoLog === true) infoLog = (...a) => console.log(new Date(), ...a);
let self = await super.load(rest);
self._etherscanApiKey = undefined;
self._privateKey = undefined;
self.infoLog = infoLog;
self.gasToken = gasToken;
self.rpc = provider._getConnection().url;
self.chain = (await provider.getNetwork()).chainId;
self.provider = provider;
self.privateKey = privateKey;
return self;
}
set etherscanApiKey(key) {
this._etherscanApiKey = key || undefined;
}
get etherscanApiKey() {
return this._etherscanApiKey || this.config.etherscan_api_key;
}
set privateKey(key) {
if (!key) {
this._privateKey = undefined;
} else {
if (!(key instanceof ethers.SigningKey)) {
key = new ethers.SigningKey(key);
}
this._privateKey = key;
this.infoLog?.(`Deployer: ${ansi('36', this.address)}`);
}
}
get privateKey() {
return this._privateKey;
}
get address() {
return this._privateKey ? ethers.computeAddress(this._privateKey) : undefined; }
requireWallet() {
const key = this.privateKey;
if (!key) throw new Error('expected private key');
return new ethers.Wallet(key, this.provider);
}
async prepare(arg0) {
let {
args = [],
libs = {},
confirms,
...artifactLike
} = artifact_from(arg0);
let {type, abi, links, bytecode: bytecode0, cid, root, compiler} = await this.resolveArtifact(artifactLike);
if (!root) throw new Error('unsupported deployment type');
if (cid.startsWith('/')) { // if (type === 'code') {
cid = relative(root, cid);
}
let {bytecode, linked} = this.linkBytecode(bytecode0, links, libs);
const wallet = this._privateKey ? this.requireWallet() : null;
let factory = new ethers.ContractFactory(abi, bytecode, wallet);
let unsigned = await factory.getDeployTransaction(...args);
unsigned.from = wallet ? wallet.address : '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
let encodedArgs = await abi.encodeDeploy(args);
let decodedArgs = ethers.AbiCoder.defaultAbiCoder().decode(abi.deploy.inputs, encodedArgs);
const gas = await this.provider.estimateGas(unsigned);
const fees = await this.provider.getFeeData();
const wei = gas * fees.maxFeePerGas;
const approx_eth = Number(wei) / 1e18;
const self = this;
const ret = {
gas,
...fees,
wei,
eth: ethers.formatEther(wei),
root,
cid,
linked,
compiler,
decodedArgs,
encodedArgs,
deployArgs(use_private_key) {
const args = [
'create',
this.cid,
'--root', this.root,
'--rpc-url', self.rpc,
'--broadcast',
'--json'
];
if (use_private_key) {
args.push('--private-key', self.privateKey.privateKey);
} else {
args.push('--interactive');
}
if (this.linked.length) {
args.push('--libraries', ...fmt_libraries(this.linked));
}
if (this.decodedArgs.length) {
args.push('--constructor-args', ...fmt_ctor_args(this.decodedArgs));
}
return args;
},
async deploy({confirms} = {}) {
const wallet = self.requireWallet();
const t0 = Date.now();
self.infoLog?.(`Deploying to ${ansi('33', self.chain)}...`);
const {deployedTo, transactionHash} = await exec({
cmd: self.forge,
args: this.deployArgs(true),
env: {FOUNDRY_PROFILE: self.profile},
//cwd: this.root
});
this.address = deployedTo;
self.infoLog?.(`Transaction: ${ansi('36', transactionHash)}`);
const contract = new ethers.Contract(deployedTo, abi, wallet);
self.infoLog?.(`Waiting for confirmation...`);
await self.provider.waitForTransaction(transactionHash, confirms);
const receipt = await self.provider.getTransactionReceipt(transactionHash);
self.infoLog?.(`Deployed: ${ansi('36', deployedTo)} (${fmt_dur(Date.now() - t0)})`);
return {contract, receipt};
},
async json() {
const args = [
'verify-contract',
ethers.ZeroAddress,
this.cid,
'--root', this.root,
'--show-standard-json-input',
];
if (this.decodedArgs.length) {
args.push('--constructor-args', encodedArgs);
}
if (this.linked.length) {
args.push('--libraries', ...fmt_libraries(this.linked));
}
const json = await exec({
cmd: self.forge,
args,
env: {FOUNDRY_PROFILE: self.profile},
//cwd: this.root
});
if (type === 'code') {
// we gotta unfuck these relatives
json.sources = Object.fromEntries(Object.entries(json.sources).map(([k, v]) => {
return [k.startsWith('../') ? normalize(join(this.root, k)) : k, v];
}));
}
return json;
},
async verifyEtherscan(a = {}) {
const {cid, encodedArgs, compiler, address} = this;
return self.verifyEtherscan({
address,
...a,
cid,
encodedArgs,
compiler,
json: await this.json(),
});
}
};
if (this.infoLog) {
// remove contract name if same as file name
this.infoLog(`Contract: ${ansi('93', cid.replace(/\/(.*)\.sol:\1$/, (_, x) => `/${x}.sol`))}`);
let stats = [
`${ansi('33', bytecode.length)}bytes`,
`${ansi('33', gas)}gas`,
`${ansi('33', (Number(fees.maxFeePerGas) / 1e9).toFixed(1))}gwei`,
`${ansi('32', approx_eth.toFixed(4))}eth`,
];
if (this.gasToken === 'ETH') {
try {
const res = await fetch('https://api.coinbase.com/v2/exchange-rates');
const {data: {rates: {ETH}}} = await res.json();
if (ETH > 0) {
stats.push(`${ansi('32', '$' + (approx_eth / ETH).toFixed(2))} @ ${(1 / ETH).toFixed(2)}`);
}
} catch (err) {
}
}
this.infoLog(...stats);
}
return ret;
}
async verifyEtherscan({json, cid, address, apiKey, encodedArgs, compiler, pollMs = 5000, retry = 10} = {}) {
const t0 = Date.now();
apiKey ??= this.etherscanApiKey;
if (!apiKey) throw new Error(`expected etherscan api key`);
address = to_address(address);
if (!address) throw new Error('expected address');
encodedArgs = encodedArgs ? ethers.hexlify(encodedArgs) : '0x';
cid ??= Object.keys(json.sources)[0]; // use first contract?
// fix this shit
if (!compiler) throw new Error('expected compiler/version');
if (is_exact_semver(compiler)) compiler = await this.compiler(compiler);
if (!compiler.startsWith('v')) compiler = `v${compiler}`;
const url = new URL('https://api.etherscan.io/v2/api');
url.searchParams.set('chainid', this.chain.toString());
url.searchParams.set('module', 'contract');
url.searchParams.set('action', 'verifysourcecode');
url.searchParams.set('apikey', apiKey);
const body = new FormData();
body.set('chainId', this.chain.toString());
body.set('sourceCode', JSON.stringify(json));
body.set('codeformat', 'solidity-standard-json-input');
body.set('contractaddress', address);
body.set('contractname', cid);
body.set('compilerversion', compiler);
body.set('constructorArguments', encodedArgs.slice(2));
this.infoLog?.('Requesting verification...');
let guid;
while (true) {
const {message, result} = await fetch(url, {method: 'POST', body}).then(r => r.json());
if (message === 'OK') {
guid = result;
break;
} else if (/unable to locate contract/i.test(result)) {
if (retry > 0) {
--retry;
this.infoLog?.(`Waiting for indexer...`);
await new Promise(ful => setTimeout(ful, pollMs));
} else {
throw error_with(`expected contract` , {chain: this.chain, address});
}
} else {
throw new Error(`etherscan: ${result}`, {result});
}
}
this.infoLog?.(`Request: ${ansi('33', guid)}`);
url.searchParams.set('guid', guid);
url.searchParams.set('action', 'checkverifystatus');
while (true) {
const {message, result} = await fetch(url).then(r => r.json());
if (message === 'OK') {
break;
} else if (/already verified/i.test(result)) {
break;
} else if (/pending in queue/i.test(result)) {
this.infoLog?.(`Waiting for verification...`);
await new Promise(ful => setTimeout(ful, pollMs));
} else {
throw error_with(`etherscan: ${result}`, {result, guid, url: url.toString()});
}
}
this.infoLog?.(`Verified: ${ansi('36', address)} (${fmt_dur(Date.now() - t0)})`);
}
}
function fmt_dur(t) {
if (t < 600) {
return `${t.toFixed(0)}ms`;
} else {
return `${(t / 1000).toFixed(1)}sec`;
}
}
function fmt_libraries(linked) {
return linked.map(({file, contract, address}) => {
return `${file}:${contract}:${address}`;
});
}
function fmt_ctor_args(args) {
return args.map((x) => {
if (Array.isArray(x)) {
return `[${fmt_ctor_args(x).join(',')}]`;
} else {
switch (typeof x) {
case 'boolean':
case 'number':
case 'bigint':
return String(x);
case 'string':
return x; //JSON.stringify(x);
default:
throw new Error(`unexpected arg: ${x}`);
}
}
});
}
class Foundry extends FoundryBase {
static of(x) {
if (!has_key(x, Symbol_foundry)) throw new TypeError(`expected Contract or Wallet`);
return x[Symbol_foundry];
}
static async launch({
port = 0,
wallets = [DEFAULT_WALLET],
anvil = 'anvil',
chain,
infiniteCallGas,
gasLimit,
blockSec,
autoClose = true,
genesisTimestamp,
hardfork = 'latest',
backend = 'ethereum',
fork,
procLog,
infoLog = true,
...rest
} = {}) {
let self = await this.load(rest);
if (!infoLog) infoLog = undefined;
if (infoLog === true) infoLog = console.log.bind(console);
if (!procLog) procLog = undefined;
if (backend !== 'ethereum' && backend !== 'optimism') {
throw error_with(`unknown backend: ${backend}`, {backend});
}
hardfork = hardfork.toLowerCase().trim();
if (hardfork === 'forge') {
hardfork = this.config.evm_version;
}
if (procLog === true) procLog = console.log.bind(console);
return new Promise((ful, rej) => {
let args = [
'--port', port,
'--accounts', 0, // create accounts on demand
];
if (chain) args.push('--chain-id', chain);
if (blockSec) args.push('--block-time', blockSec);
if (infiniteCallGas) {
//args.push('--disable-block-gas-limit');
// https://github.com/foundry-rs/foundry/pull/6955
// currently bugged
// 20240819: still bugged
// https://github.com/foundry-rs/foundry/pull/8274
// 20240827: yet another bug
// https://github.com/foundry-rs/foundry/issues/8759
// 20241026: appears fixed
args.push('--disable-block-gas-limit');
} else if (gasLimit) {
args.push('--gas-limit', gasLimit);
}
if (fork) {
fork = String(fork);
args.push('--fork-url', fork);
}
if (genesisTimestamp !== undefined) {
args.push('--timestamp', genesisTimestamp);
}
if (hardfork !== 'latest') {
args.push('--hardfork', hardfork);
}
if (backend === 'optimism') {
args.push('--optimism');
}
let proc = spawn(anvil, args, {
env: {...process.env, RUST_LOG: 'node=info'},
stdio: ['ignore', 'pipe', 'pipe'],
});
const fail = data => {
proc.kill();
let error = strip_ansi(data.toString()).trim();
let title = 'unknown launch error';
let match = error.match(/^Error: (.*)/);
if (match) title = match[1];
rej(error_with(title, {args, error}));
};
proc.stderr.once('data', fail);
let lines = [];
const waiter = on_newline(line => {
lines.push(line);
// 20240319: there's some random situation where anvil doesnt
// print a listening endpoint in the first stdout flush
let match = line.match(/^Listening on (.*)$/);
if (match) init(lines.join('\n'), match[1]);
// does this need a timeout?
});
proc.stdout.on('data', waiter);
async function init(bootmsg, host) {
proc.stdout.removeListener('data', waiter);
proc.stderr.removeListener('data', fail);
if (autoClose) {
const kill = () => proc.kill();
process.on('exit', kill);
proc.once('exit', () => process.removeListener('exit', kill));
}
if (is_pathlike(infoLog)) {
let console = new Console(createWriteStream(infoLog));
infoLog = console.log.bind(console);
}
if (is_pathlike(procLog)) {
let out = createWriteStream(procLog);
out.write(bootmsg + '\n');
proc.stdout.pipe(out);
procLog = false;
} else if (procLog) {
procLog(bootmsg);
}
let show_log = true; // 20240811: foundry workaround for gas estimation spam
proc.stdout.on('data', on_newline(line => {
// https://github.com/foundry-rs/foundry/issues/7681
// https://github.com/foundry-rs/foundry/issues/8591
// [2m2024-08-02T19:38:31.399817Z[0m [32m INFO[0m [2mnode::user[0m[2m:[0m anvil_setLoggingEnabled
let match = line.match(/^(\x1B\[\d+m\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\x1B\[0m) \x1B\[\d+m([^\x1B]+)\x1B\[0m \x1B\[\d+m([^\x1B]+)\x1B\[0m\x1B\[2m:\x1B\[0m (.*)$/);
if (match) {
let [_, time, _level, kind, line] = match;
if (kind === 'node::user') {
// note: this gets all fucky when weaving promises
// but i dont know of any work around until this is fixed
show_log = line !== 'eth_estimateGas';
} else if (kind === 'node::console') {
if (show_log) {
self.emit('console', line);
infoLog?.(TAG_CONSOLE, time, line);
}
return;
}
}
procLog?.(line);
}));
let endpoint = `ws://${host}`;
port = parseInt(host.slice(host.lastIndexOf(':') + 1));
let provider = new ethers.WebSocketProvider(endpoint, chain, {staticNetwork: true});
//let provider = new ethers.IpcSocketProvider('/tmp/anvil.ipc', chain, {staticNetwork: true});
chain ??= parseInt(await provider.send('eth_chainId')); // determine chain id
let automine = !!await provider.send('anvil_getAutomine');
if (automine) {
provider.destroy();
provider = new ethers.WebSocketProvider(endpoint, chain, {staticNetwork: true, cacheTimeout: -1});
}
Object.assign(self, {
anvil, proc, provider,
infoLog, procLog,
endpoint, chain, port, fork,
automine, hardfork, backend,
started: new Date(),
ensRegistry: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
});
wallets = await Promise.all(wallets.map(x => self.ensureWallet(x)));
infoLog?.(TAG_START, self.pretty({chain, endpoint, wallets}));
proc.once('exit', () => {
self.emit('shutdown');
infoLog?.(TAG_STOP, `${ansi('33', fmt_dur(Date.now() - self.started))}`);
});
ful(self);
}
});
}
constructor() {
super();
this.accounts = new Map();
this.write_map = new Map();
this.event_map = new Map();
const error_map = this.error_map = new Map();
this.wallets = {};
this.error_fixer = function(data, tx) {
const error0 = this[Symbol_makeErrors](data, tx);
if (!error0.reason) {
let bucket = error_map.get(ethers.dataSlice(data, 0, 4));
if (bucket) {
for (let abi of bucket.values()) {
let error = abi.makeError(data, tx);
if (error.reason) {
error.invocation ??= error0.invocation;
return error;
}
}
}
}
return error0;
};
this.shutdown = () => {
if (!this.killed) {
this.killed = new Promise(ful => {
this.provider.destroy();
this.proc.once('exit', ful);
this.proc.kill();
});
}
return this.killed;
};
}
nextBlock({blocks = 1, sec = 1} = {}) {
return this.provider.send('anvil_mine', [
ethers.toBeHex(blocks),
ethers.toBeHex(sec),
]);
}
async setStorageValue(a, slot, value) {
if (value instanceof Uint8Array) {
if (value.length != 32) throw new TypeError(`expected exactly 32 bytes`);
value = ethers.hexlify(value);
} else {
value = ethers.toBeHex(value || 0, 32);
}
await this.provider.send('anvil_setStorageAt', [to_address(a), ethers.toBeHex(slot, 32), value]);
}
async getStorageBytesLength(a, slot) {
return parse_bytes_length(await this.provider.getStorage(a, slot));
}
async getStorageBytes(a, slot, maxBytes = 4096) {
slot = BigInt(slot);
const header = await this.provider.getStorage(a, slot);
const size = parse_bytes_length(header);
if (maxBytes && Number(size) > maxBytes) throw new Error(`too large: ${size} > ${maxBytes}`);
if (size < 32) return ethers.getBytes(header).slice(0, Number(size));
const v = new Uint8Array(Number(size)); // throws if huge
let off = BigInt(ethers.solidityPackedKeccak256(['uint256'], [slot]));
const ps = [];
for (let i = 0; i < v.length; i += 32) {
const pos = i;
ps.push(this.provider.getStorage(a, off++).then(x => {
let u = ethers.getBytes(x);
const n = v.length - pos;
if (n < 32) u = u.subarray(0, n);
v.set(u, pos);
}));
}
await Promise.all(ps);
return v;
}
async setStorageBytes(a, slot, v, zeroBytes = true) {
slot = BigInt(slot);
v = v ? ethers.getBytes(v) : new Uint8Array(0);
let off = BigInt(ethers.solidityPackedKeccak256(['uint256'], [slot]));
let offEnd = 0n;
if (zeroBytes) {
if (zeroBytes === true) zeroBytes = 4096;
const size = await this.getStorageBytesLength(a, slot);
if (Number(size) > zeroBytes) throw new Error(`prior size too large: ${size} > ${zeroBytes}`);
offEnd = off + ((size + 31n) >> 5n);
}
const ps = [];
if (v.length < 32) {
const u = new Uint8Array(32);
u.set(v);
u[31] = v.length << 1;
ps.push(this.setStorageValue(a, slot, u));
} else {
ps.push(this.setStorageValue(a, slot, (v.length << 1) | 1));
for (let pos = 0; pos < v.length; ) {
const end = pos + 32;
if (end > v.length) {
const u = new Uint8Array(32);
u.set(v.subarray(pos));
ps.push(this.setStorageValue(a, off++, u));
} else {
ps.push(this.setStorageValue(a, off++, v.subarray(pos, end)));
}
pos = end;
}
}
while (off < offEnd) {
ps.push(this.setStorageValue(a, off++, 0));
}
await Promise.all(ps);
}
async overrideENS({name, node, owner, resolver, registry = this.ensRegistry}) {
// https://etherscan.io/address/0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e
const slot = BigInt(ethers.solidityPackedKeccak256(["bytes32", "uint256"], [node ?? ethers.namehash(name), 0n]));
function coerce_address(x) {
if (x === null) return 0;
const a = to_address(x);
if (!a) throw new TypeError(`expected address: ${x}`);
return a;
}
owner = coerce_address(owner);
resolver = coerce_address(resolver);
if (resolver !== undefined && BigInt(resolver)) {
// BUG: https://github.com/foundry-rs/foundry/issues/9743
// for some reason, the owner needs to be nonzero if the resolver is set
if (owner === undefined) {
owner = BigInt(await this.provider.getStorage(registry, slot));
}
if (!BigInt(owner)) owner = 1;
}
await Promise.all([
owner !== undefined && this.setStorageValue(registry, slot, owner),
resolver !== undefined && this.setStorageValue(registry, slot + 1n, resolver)
]);
}
requireWallet(...xs) {
for (let x of xs) {
if (!x) continue;
if (x instanceof ethers.Wallet) {
if (x[Symbol_foundry] === this) return x;
throw error_with('unowned wallet', {wallet: x});
}
let address = to_address(x);
if (address) {
let a = this.accounts.get(address);
if (a) return a;
} else if (typeof x === 'string') {
let a = this.wallets[x];
if (a) return a;
}
throw error_with('expected wallet', {wallet: x});
}
throw new Error('missing required wallet');
}
randomWallet({prefix = 'random', ...a} = {}) {
let id = 0;
while (true) {
let name = `${prefix}${++id}`; // TODO fix O(n)
if (!this.wallets[name]) {
return this.ensureWallet(name, a);
}
}
}
async ensureWallet(x, {ether = 10000} = {}) {
if (x instanceof ethers.Wallet) return this.requireWallet(x);
if (!x || typeof x !== 'string' || is_address(x)) {
throw error_with('expected wallet name', {name: x});
}
let wallet = this.wallets[x];
if (!wallet) {
wallet = new ethers.Wallet(ethers.id(x), this.provider);
ether = BigInt(ether);
if (ether > 0) {
await this.provider.send('anvil_setBalance', [wallet.address, ethers.toBeHex(ether * BigInt(1e18))]);
}
wallet[Symbol_name] = x;
wallet[Symbol_foundry] = this;
wallet.toString = get_NAME;
this.wallets[x] = wallet;
this.accounts.set(wallet.address, wallet);
}
return wallet;
}
pretty(x) {
if (x) {
if (typeof x === 'object') {
if (Symbol_foundry in x) {
return {
[inspect.custom]() {
return ansi('35', x[Symbol_name]);
}
};
} else if (x instanceof ethers.Indexed) {
return {
[inspect.custom]() {
return ansi('36', `'${x.hash}'`);
}
};
} else if (Array.isArray(x)) {
return x.map(y => this.pretty(y));
} else if (x.constructor === Object) {
return Object.fromEntries(Object.entries(x).map(([k, v]) => [k, this.pretty(v)]));
}
} else if (typeof x === 'string') {
if (is_address(x)) {
let a = this.accounts.get(x);
if (a) return this.pretty(a);
}
}
}
return x;
}
parseError(err) {
// TODO: fix me
if (err.code === 'CALL_EXCEPTION') {
let {data} = err;
console.log(this.error_map);
let bucket = this.error_map.get(data.slice(0, 10));
console.log('bucket', bucket);
if (bucket) {
for (let abi of bucket.values()) {
try {
return abi.parseError(data);
} catch (err) {
}
}
}
}
}
parseTransaction(tx) {
let bucket = this.write_map.get(tx.data?.slice(0, 10));
if (!bucket) return;
for (let abi of bucket.values()) {
let desc = abi.parseTransaction(tx);
if (desc) return desc;
}
}
async confirm(p, {silent, confirms, ...extra} = {}) {
let tx = await p;
let receipt = await tx.wait(confirms);
let desc = this.parseTransaction(tx);
if (!silent && this.infoLog) {
let args = {gas: receipt.gasUsed, ...extra};
let action;
if (desc) {
Object.assign(args, desc.args.toObject());
action = desc.signature;
} else if (tx.data?.length >= 10) {
action = ansi('90', tx.data.slice(0, 10));
if (tx.data.length > 10) {
args.calldata = '0x' + tx.data.slice(10);
}
}
if (tx.value > 0) {
args.value = tx.value;
}
if (action) {
this.infoLog(TAG_TX, this.pretty(receipt.from), '>>', this.pretty(receipt.to), action, this.pretty(args));
} else {
this.infoLog(TAG_TX, this.pretty(receipt.from), '>>', this.pretty(receipt.to), this.pretty(args));
}
this._dump_logs(receipt);
}
this.emit('tx', tx, receipt, desc);
return receipt;
}
_dump_logs(receipt) {
for (let x of receipt.logs) {
let abi = this.event_map.get(x.topics[0]);
let event;
if (abi) {
event = abi.parseLog(x);
}
if (event) {
if (event.args.length) {
this.infoLog(TAG_EVENT, event.signature, this.pretty(event.args.toObject()));
} else {
this.infoLog(TAG_EVENT, event.signature);
}
}
}
}
async attach(args0) {
let {
to,
from = DEFAULT_WALLET,
abis = [],
parseAllErrors = true,
...artifactLike
} = args0;
from = await this.ensureWallet(from);
let {abi: abi0, contract} = await this.resolveArtifact(artifactLike);
let abi = mergeABI(abi0, ...abis);
this.addABI(abi);
if (parseAllErrors) abi = this.parseAllErrors(abi);
let c = new ethers.Contract(to_address(to), abi, from);
c[Symbol_name] = `${contract}<${smol_addr(c.target)}>`;
c[Symbol_foundry] = this;
c.__info = {contract};
c.toString = get_NAME;
this.accounts.set(c.target, c);
return c;
}
async deploy(arg0) {
let {
from = DEFAULT_WALLET,
args = [],
libs = {},
abis = [],
confirms,
silent = false,
parseAllErrors = true,
...artifactLike
} = artifact_from(arg0);
from = await this.ensureWallet(from);
let {abi: abi0, links, bytecode: bytecode0, origin, contract, type} = await this.resolveArtifact(artifactLike);
if (type == 'bytecode' && !args.length && abi0.deploy.inputs.length) {
abi0 = new ethers.Interface(abi0.fragments.filter(x => x !== abi0.deploy)); // remove constructor
}
let abi = mergeABI(abi0, ...abis);
this.addABI(abi);
if (parseAllErrors) abi = this.parseAllErrors(abi);
let {bytecode, linkedLibs} = this.linkBytecode(bytecode0, links, libs);
let factory = new ethers.ContractFactory(abi, bytecode, from);
let unsigned = await factory.getDeployTransaction(...args);
let tx = await from.sendTransaction(unsigned);
let receipt = new ethers.ContractTransactionReceipt(abi, this.provider, await tx.wait(confirms));
let c = new ethers.Contract(receipt.contractAddress, abi, from);
c[Symbol_name] = `${contract}<${smol_addr(c.target)}>`; // so we can deploy the same contract multiple times
c[Symbol_foundry] = this;
c.toString = get_NAME;
let code = ethers.getBytes(await this.provider.getCode(c.target));
c.__info = {contract, origin, code, libs: linkedLibs, from};
c.__receipt = receipt;
this.accounts.set(c.target, c);
if (!silent && this.infoLog) {
let stats = [
`${ansi('33', code.length)}bytes`,
`${ansi('33', receipt.gasUsed)}gas`,
];
if (Object.keys(linkedLibs).length) {
stats.push(this.pretty(linkedLibs));
}
this.infoLog(TAG_DEPLOY, this.pretty(from), origin, this.pretty(c), ...stats);
this._dump_logs(receipt);
}
this.emit('deploy', c); // tx, receipt?
return c;
}
addABI(abi) {
abi.forEachFunction(f => {
if (f.constant) return;
let bucket = this.write_map.get(f.selector);
if (!bucket) {
bucket = new Map();
this.write_map.set(f.selector, bucket);
}
bucket.set(f.format('sighash'), abi);
});
abi.forEachEvent(e => this.event_map.set(e.topicHash, abi));
abi.forEachError(e => {
let bucket = this.error_map.get(e.selector);
if (!bucket) {
bucket = new Map();
this.error_map.set(e.selector, bucket);
}
bucket.set(ethers.id(e.format('sighash')), abi);
});
}
async parseArtifacts() {
for (const {abi} of await this.artifacts()) {
this.addABI(abi);
}
}
parseAllErrors(abi) {
if (abi.makeError !== this.error_fixer) {
abi[Symbol_makeErrors] = abi.makeError.bind(abi);
abi.makeError = this.error_fixer;
}
return abi;
}
findEvent(event) {
if (event instanceof ethers.EventFragment) { // solo fragment
try {
return this.findEvent(event.topicHash);
} catch (err) {
return {
abi: new ethers.Interface([event]),
frag: event
};
}
}
if (event.includes('(')) { // signature => topicHash
event = ethers.EventFragment.from(event).topicHash;
}
if (/^0x[0-9a-f]{64}$/i.test(event)) { // topicHash
let topic = event.toLowerCase();
let abi = this.event_map.get(topic);
if (abi) {
return {abi, frag: abi.getEvent(topic)};
}
} else { // name
let matches = new Set();
let first;
for (let abi of this.event_map.values()) {
abi.forEachEvent(frag => {
if (frag.name === event) {
if (!first) first = {abi, frag};
matches.add(frag.topicHash);
}
});
}
if (matches.size > 1) throw error_with(`multiple events: ${event}`, {event, matches})
if (first) {
return first;
}
}
throw error_with(`unknown event: ${event}`, {event});
}
getEventResults(logs, event) {
if (logs instanceof ethers.Contract && logs[Symbol_foundry]) {
logs = logs.__receipt.logs;
} else if (logs instanceof ethers.TransactionReceipt) {
logs = logs.logs;
}
if (!Array.isArray(logs)) throw new TypeError('unable to coerce logs');
let {abi, frag} = this.findEvent(event);
let found = [];
for (const log of logs) {
try {
let desc = abi.parseLog(log);
if (desc.fragment === frag) {
found.push(desc.args);
}
} catch (err) {
}
}
return found;
//throw error_with(`missing event: ${frag.name}`, {logs, abi, frag});
}
}
function abi_from_solc_json(json) {
// purge stuff that ethers cant parse
// TODO: check that this is an external library
// https://github.com/ethereum/solidity/issues/15470
let v = [];
for (let x of json) {
try {
v.push(ethers.Fragment.from(x));
} catch (err) {
}
}
return new ethers.Interface(v);
}
function iface_from(x) {
return x instanceof ethers.BaseContract ? x.interface : ethers.Interface.from(x);
}
function artifact_from(x) {
return typeof x === 'string' ? x.startsWith('0x') ? {bytecode: x} : {sol: x} : x;
}
function mergeABI(...a) {
if (a.length < 2) return iface_from(a[0] ?? []);
let unique = new Map();
let extra = [];
a.forEach((x, i) => {
for (let f of iface_from(x).fragments) {
switch (f.type) {
case 'constructor':
case 'fallback':
if (!i) extra.push(f);
break;
case 'function':
case 'event':
case 'error': // take all
let key = `${f.type}:${f.format()}`;
if (key && !unique.has(key)) {
unique.set(key, f);
}
break;
}
}
});
return new ethers.Interface([...extra, ...unique.values()]);
}
function filter_errors(errors) {
return errors.filter(x => x.severity === 'error');
}
function extract_links(linkReferences) {
return Object.entries(linkReferences).flatMap(([file, links]) => {
return Object.entries(links).map(([contract, ranges]) => {
let offsets = ranges.map(({start, length}) => {
if (length != 20) throw error_with(`expected 20 bytes`, {file, contract, start, length});
return start;
});
return {file, contract, offsets};
});
});
}
function parse_bytes_length(header) {
header = BigInt(header);
let size = header >> 1n;
if (header & 1n) {
if (size < 32n) throw new Error(`invalid large bytes encoding: ${size} < 32`);
} else {
size &= 255n;
if (size >= 32n) throw new Error(`invalid small bytes encoding: ${size} > 31`);
}
return size;
}
function split(s) {
return s ? s.split('.') : [];
}
class Node extends Map {
static create(name) {
return name instanceof this ? name : this.root().create(name);
}
static root(tag = 'root') {
return new this(null, ethers.ZeroHash, `[${tag}]`);
}
constructor(parent, namehash, label, labelhash) {
super();
this.parent = parent;
this.namehash = namehash;
this.label = label;
this.labelhash = labelhash;
}
get dns() {
return ethers.getBytes(ethers.dnsEncode(this.name, 255));
}
get name() {
if (!this.parent) return '';
let v = [];
for (let x = this; x.parent; x = x.parent) v.push(x.label);
return v.join('.');
}
get depth() {
let n = 0;
for (let x = this; x.parent; x = x.parent) ++n;
return n;
}
get nodeCount() {
let n = 0;
this.scan(() => ++n);
return n;
}
get root() {
let x = this;
while (x.parent) x = x.parent;
return x;
}
get isETH2LD() {
return this.parent?.name === 'eth';
}
path(inc_root) {
// raffy.eth => [raffy.eth, eth, <root>?]
let v = [];
for (let x = this; inc_root ? x : x.parent; x = x.parent) v.push(x);
return v;
}
find(name) {
return split(name).reduceRight((n, s) => n?.get(s), this);
}
create(name) {
return split(name).reduceRight((n, s) => n.child(s), this);
}
child(label) {
let node = this.get(label);
if (!node) {
let labelhash = ethers.id(label);
let namehash = ethers.solidityPackedKeccak256(['bytes32', 'by