Skip to content

Commit af5bd96

Browse files
authored
feat: populate script (#36)
1 parent 4e346aa commit af5bd96

12 files changed

Lines changed: 1285 additions & 2 deletions

File tree

.github/workflows/tests.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,37 @@ jobs:
6464
- name: Run tests
6565
run: yarn test:integration
6666

67+
populate-script-tests:
68+
name: Run Populate Script Tests
69+
runs-on: ubuntu-latest
70+
steps:
71+
- uses: actions/checkout@v4
72+
73+
- name: Install Foundry
74+
uses: foundry-rs/foundry-toolchain@v1
75+
with:
76+
version: stable
77+
78+
- name: Use Node.js
79+
uses: actions/setup-node@v4
80+
with:
81+
node-version: 20.x
82+
cache: "yarn"
83+
84+
- name: Install dependencies
85+
run: yarn --frozen-lockfile
86+
87+
- name: Precompile
88+
run: yarn build
89+
90+
- name: Install dependencies (populate-script)
91+
working-directory: populate-script
92+
run: yarn --frozen-lockfile
93+
94+
- name: Run tests (populate-script)
95+
working-directory: populate-script
96+
run: yarn test
97+
6798
# medusa-tests:
6899
# name: Medusa Test
69100
# runs-on: ubuntu-latest

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,30 @@ Then `setRecord()` must be called with the parameters mentioned above. More info
5050

5151
Then `setRecord()` must be called with the parameters mentioned above. More information [here](https://eips.ethereum.org/EIPS/eip-7828#reverse-resolution-step-by-step-example)
5252

53+
## Populate Script
54+
55+
To populate a file with the `setRecords` calldatas for the available chains in [https://github.com/ethereum-lists/chains](https://github.com/ethereum-lists/chains), run:
56+
```bash
57+
cd populate-script
58+
yarn install
59+
yarn populate
60+
```
61+
62+
The script will populate a file in [populate-script/out/batches.json](populate-script/out/batches.json) with the following format:
63+
64+
```json
65+
[
66+
{
67+
"chainIds": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
68+
"forwardCalldata": "0x57d...",
69+
"reverseCalldata": "0x57dcc7..."
70+
},
71+
//...
72+
]
73+
```
74+
75+
The file is separated in batches of max `10` chains. Each batch contains the `setRecords` calldata (forward and reverse) for the chains in the `chainIds` array.
76+
5377
## Specification
5478

5579
The complete technical specification can be found [here](https://github.com/ethereum/L2-interop/blob/main/specs/addresses/L2Resolver.md).

foundry.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ sort_imports = true
1212
solc_version = '0.8.30'
1313
libs = ['node_modules', 'lib']
1414
optimizer_runs = 10_000
15+
fs_permissions = [{ access = "read", path = "./populate-script/out/batches.json" }]
1516

1617
[profile.optimized]
1718
via_ir = true

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
"lint:fix": "forge fmt && yarn lint:sol --fix && sort-package-json",
2222
"lint:natspec": "lintspec",
2323
"lint:sol": "solhint 'src/**/*.sol' 'script/**/*.sol' 'test/**/*.sol'",
24+
"populate": "forge compile && cd populate-script && yarn && yarn populate",
2425
"prepare": "husky",
25-
"test": "forge test -vvv",
26+
"test": "yarn populate && forge test -vvv",
2627
"test:bulloak:fix": "bulloak check --fix ./**/*.tree",
2728
"test:bulloak:scaffold": "bulloak scaffold -wS ./**/*.tree",
2829
"test:fuzz": "medusa fuzz",
29-
"test:integration": "forge test --match-contract Integration -vvv",
30+
"test:integration": "yarn populate && forge test --match-contract Integration -vvv",
3031
"test:symbolic": "halmos",
3132
"test:unit": "forge test --match-contract Unit -vvv",
3233
"test:unit:deep": "FOUNDRY_FUZZ_RUNS=5000 yarn test:unit"

populate-script/.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# General
2+
yarn-error.log
3+
node_modules
4+
.DS_STORE
5+
.vscode
6+
7+
# Config files
8+
.env
9+
10+
# Avoid ignoring gitkeep
11+
!/**/.gitkeep
12+
13+
# Output batches file
14+
out/batches.json
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[
2+
{
3+
"type": "function",
4+
"name": "setRecords",
5+
"stateMutability": "nonpayable",
6+
"inputs": [
7+
{ "name": "_nodes", "type": "bytes32[]" },
8+
{ "name": "_keys", "type": "string[]" },
9+
{ "name": "_values", "type": "bytes[]" }
10+
],
11+
"outputs": []
12+
}
13+
]

populate-script/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "populate-script",
3+
"version": "1.0.0",
4+
"license": "MIT",
5+
"main": "script.js",
6+
"scripts": {
7+
"populate": "ts-node script.ts",
8+
"test": "vitest run"
9+
},
10+
"devDependencies": {
11+
"@types/node": "24.3.0",
12+
"ts-node": "10.9.2",
13+
"typescript": "5.9.2",
14+
"viem": "2.36.0",
15+
"vitest": "3.2.4"
16+
}
17+
}

populate-script/script.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { mkdir, writeFile } from 'node:fs/promises';
2+
import { join } from 'node:path';
3+
import { encodeAbiParameters, encodeFunctionData, Hex, hexToBytes, keccak256, namehash, numberToHex, stringToBytes, toHex } from 'viem';
4+
import L2ResolverAbi from '../out/L2Resolver.sol/L2Resolver.json';
5+
6+
type ChainItem = {
7+
chainId: number;
8+
subDomain: string;
9+
shortName: string;
10+
};
11+
12+
type BatchOutput = {
13+
chainIds: number[];
14+
forwardCalldata: Hex;
15+
reverseCalldata: Hex;
16+
};
17+
18+
const SOURCE_URL: string = "https://chainid.network/chains_mini.json";
19+
const BATCH_SIZE: number = 10;
20+
const L2_SUFFIX: string = '.l2.eth';
21+
const CHAIN_IDENTIFIER_EIP7930_KEY: string = 'chain.id.eip7930';
22+
const REVERSE_LOOKUP_NODE: Hex = keccak256(stringToBytes('reverse.chain.id.eip7930')) as Hex;
23+
24+
/**
25+
* Processes a batch of chain items and returns the chainIds, forwardCalldata, and reverseCalldata for setRecords.
26+
* @param batch - The batch of chain items to process.
27+
* @returns The chainIds, forwardCalldata, and reverseCalldata.
28+
*/
29+
const processBatch = async (
30+
batch: ChainItem[]
31+
): Promise<[number[], Hex, Hex]> => {
32+
const forwardNodes: Hex[] = [];
33+
const forwardKeys: string[] = [];
34+
const forwardValues: Hex[] = [];
35+
36+
const reverseNodes: Hex[] = [];
37+
const reverseKeys: string[] = [];
38+
const reverseValues: Hex[] = [];
39+
40+
const chainIds: number[] = [];
41+
42+
for (const item of batch) {
43+
const subDomain = item.subDomain || item.shortName;
44+
if (subDomain === '' || subDomain === null) {
45+
throw new Error('Subdomain cannot be empty');
46+
}
47+
const domain = `${subDomain}${L2_SUFFIX}`.toLowerCase();
48+
49+
chainIds.push(item.chainId);
50+
51+
const nodeNamehash = namehash(domain);
52+
const chainIdBytes = encodeEIP7930ChainIdBytes(item.chainId);
53+
54+
forwardNodes.push(nodeNamehash);
55+
forwardKeys.push(CHAIN_IDENTIFIER_EIP7930_KEY);
56+
forwardValues.push(chainIdBytes);
57+
58+
const identifierHash = keccak256(chainIdBytes);
59+
const reverseKey = `${CHAIN_IDENTIFIER_EIP7930_KEY}${identifierHash}`;
60+
const encodedDomain = encodeAbiParameters([{ type: 'string' }], [domain]);
61+
62+
reverseNodes.push(REVERSE_LOOKUP_NODE);
63+
reverseKeys.push(reverseKey);
64+
reverseValues.push(encodedDomain);
65+
}
66+
67+
const forwardCalldata = encodeFunctionData({
68+
abi: L2ResolverAbi.abi,
69+
functionName: 'setRecords',
70+
args: [forwardNodes, forwardKeys, forwardValues],
71+
});
72+
73+
const reverseCalldata = encodeFunctionData({
74+
abi: L2ResolverAbi.abi,
75+
functionName: 'setRecords',
76+
args: [reverseNodes, reverseKeys, reverseValues],
77+
});
78+
79+
return [chainIds, forwardCalldata, reverseCalldata];
80+
};
81+
82+
const fetchChains = async (): Promise<ChainItem[]> => {
83+
const response = await fetch(SOURCE_URL);
84+
if (!response.ok) {
85+
throw new Error(`Failed to fetch chains: ${response.status} ${response.statusText}`);
86+
}
87+
return (await response.json()) as ChainItem[];
88+
};
89+
90+
const filteredChainItem = (item: ChainItem): ChainItem => {
91+
return {
92+
chainId: item.chainId,
93+
subDomain: item.subDomain,
94+
shortName: item.shortName,
95+
};
96+
};
97+
98+
const chunkArray = <ChainItem>(items: ChainItem[], chunkSize: number): ChainItem[][] => {
99+
const chunks: ChainItem[][] = [];
100+
for (let i = 0; i < items.length; i += chunkSize) {
101+
chunks.push(items.slice(i, i + chunkSize));
102+
}
103+
return chunks;
104+
};
105+
106+
/**
107+
* Encodes the chainId to the bytes format required by EIP-7930.
108+
* @param chainId - The chainId to encode.
109+
* @returns The encoded bytes.
110+
*/
111+
const encodeEIP7930ChainIdBytes = (chainId: number): Hex => {
112+
const version = Uint8Array.of(0x00, 0x01);
113+
const chainType = Uint8Array.of(0x00, 0x00);
114+
const chainRef = hexToBytes(numberToHex(chainId));
115+
const chainRefLength = Uint8Array.of(chainRef.length);
116+
const addressLength = Uint8Array.of(0x00);
117+
118+
const bytes = new Uint8Array(version.length + chainType.length + chainRefLength.length + chainRef.length + addressLength.length);
119+
bytes.set(version, 0);
120+
bytes.set(chainType, 2);
121+
bytes.set(chainRefLength, 4);
122+
bytes.set(chainRef, 5);
123+
bytes.set(addressLength, 5 + chainRef.length);
124+
125+
return toHex(bytes);
126+
}
127+
128+
/**
129+
* Main function to fetch chains, filter them, and process them in batches.
130+
* Writes the outputs to batches.json.
131+
*/
132+
const main = async (): Promise<void> => {
133+
const chains: ChainItem[] = await fetchChains();
134+
const records: ChainItem[] = chains.map(filteredChainItem);
135+
const batches: ChainItem[][] = chunkArray(records, BATCH_SIZE);
136+
137+
const outputs: BatchOutput[] = [];
138+
for (const batch of batches) {
139+
const [chainIds, forwardCalldata, reverseCalldata] = await processBatch(batch);
140+
outputs.push({ chainIds, forwardCalldata, reverseCalldata });
141+
}
142+
143+
await mkdir('out', { recursive: true });
144+
await writeFile(join('out', 'batches.json'), JSON.stringify(outputs, null, 2), 'utf8');
145+
};
146+
147+
main().catch((error) => {
148+
console.error(error);
149+
process.exitCode = 1;
150+
});
151+
152+
export { encodeEIP7930ChainIdBytes, processBatch };
153+
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { encodeEIP7930ChainIdBytes, processBatch } from '../script';
3+
4+
describe('processBatch', () => {
5+
it('returns chainIds and encodes forward/reverse calldata with subDomains', async () => {
6+
const batch = [
7+
{ chainId: 10, subDomain: 'optimism', shortName: '' },
8+
{ chainId: 42161, subDomain: 'arbitrum', shortName: '' },
9+
];
10+
11+
const [ids, forward, reverse] = await processBatch(batch);
12+
expect(ids).toEqual([10, 42161]);
13+
expect(forward).toEqual('0x57dcc742000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000002cd14771f519e64452fe1d5a43016d25c86ef573ec499773443e29751c0dfcebcc2442a39153d8d19fb2264129e381f3c7d3a6ce1ff1d54d1a31379a7ece272c40000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000010636861696e2e69642e65697037393330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010636861696e2e69642e6569703739333000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000700010000010a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080001000002a4b100000000000000000000000000000000000000000000000000');
14+
expect(reverse).toEqual('0x57dcc742000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000002a1e7ae8efca73939b8018c7c06321689d70c6362862a1c900eb76c9e581e4ae8a1e7ae8efca73939b8018c7c06321689d70c6362862a1c900eb76c9e581e4ae80000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000052636861696e2e69642e6569703739333030783061356130383765343530343661313434333331396132323766626466643062666336646666666332336338383561333334363439646330396233643838653200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052636861696e2e69642e6569703739333030786361626131313332626434396465333038306632613031366535613938646436363965663436613963333661333835313166646563363362313830636434646600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000f6f7074696d69736d2e6c322e657468000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000f617262697472756d2e6c322e6574680000000000000000000000000000000000');
15+
});
16+
17+
it('returns chainIds and encodes forward/reverse calldata with shortNames', async () => {
18+
const batch = [
19+
{ chainId: 10, subDomain: '', shortName: 'oeth' },
20+
{ chainId: 42161, subDomain: '', shortName: 'arb1' },
21+
];
22+
23+
const [ids, forward, reverse] = await processBatch(batch);
24+
expect(ids).toEqual([10, 42161]);
25+
expect(forward).toEqual('0x57dcc742000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000002bbbd5bca1ea05c476310b049bbc80aa7f64689f6972e4e4edbccecd388b9d6c9d3f9d2e3af6fcb8556179eaac7dca530ac24fde8a4683b0ed98eab3475d198f00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000010636861696e2e69642e65697037393330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010636861696e2e69642e6569703739333000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000700010000010a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080001000002a4b100000000000000000000000000000000000000000000000000');
26+
expect(reverse).toEqual('0x57dcc742000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000002a1e7ae8efca73939b8018c7c06321689d70c6362862a1c900eb76c9e581e4ae8a1e7ae8efca73939b8018c7c06321689d70c6362862a1c900eb76c9e581e4ae80000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000052636861696e2e69642e6569703739333030783061356130383765343530343661313434333331396132323766626466643062666336646666666332336338383561333334363439646330396233643838653200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052636861696e2e69642e6569703739333030786361626131313332626434396465333038306632613031366535613938646436363965663436613963333661333835313166646563363362313830636434646600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b6f6574682e6c322e65746800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b617262312e6c322e657468000000000000000000000000000000000000000000');
27+
});
28+
29+
it('raises an error if subDomain and shortName are empty', async () => {
30+
const batch = [
31+
{ chainId: 10, subDomain: '', shortName: 'oeth' },
32+
{ chainId: 42161, subDomain: '', shortName: '' },
33+
];
34+
try {
35+
expect(await processBatch(batch))
36+
} catch (err: any) {
37+
expect(err.message).toEqual('Subdomain cannot be empty');
38+
}
39+
});
40+
});
41+
42+
describe('encodeEIP7930ChainIdBytes', () => {
43+
it('encodes optimism chainId to bytes', async () => {
44+
const chainId = 10;
45+
const bytes = encodeEIP7930ChainIdBytes(chainId);
46+
expect(bytes).toEqual('0x00010000010a00');
47+
});
48+
49+
it('encodes arbitrum chainId to bytes', async () => {
50+
const chainId = 42161;
51+
const bytes = encodeEIP7930ChainIdBytes(chainId);
52+
expect(bytes).toEqual('0x0001000002a4b100');
53+
});
54+
});

populate-script/tsconfig.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"module": "commonjs",
5+
"esModuleInterop": true,
6+
"strict": true,
7+
"skipLibCheck": true,
8+
"resolveJsonModule": true
9+
}
10+
}

0 commit comments

Comments
 (0)