Skip to content

Commit 91f266e

Browse files
nonergodickcsongor
andauthored
basic code auto-generation (#55)
* first push to leverage ts sdk for constants * simple wrapper contract generator for library testing * use constants and apply bandaid to fix tests pending a proper solution * minor Makefile cleanup * minor cleanup/hardening * harden node_modules make rule Co-authored-by: Csongor Kiss <[email protected]> --------- Co-authored-by: Csongor Kiss <[email protected]>
1 parent efd37e5 commit 91f266e

15 files changed

+1417
-50
lines changed

gen/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

gen/Makefile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
GENEREATORS = chains cctpDomains
2+
chains_TARGET = ../src/constants/Chains.sol
3+
cctpDomains_TARGET = ../src/constants/CCTPDomains.sol
4+
5+
TEST_WRAPPERS = BytesParsing
6+
BytesParsing_BASE_PATH = libraries
7+
8+
fnTestWrapperTarget = ../test/generated/$(1)TestWrapper.sol
9+
TEST_WRAPPER_TARGETS =\
10+
$(foreach wrapper, $(TEST_WRAPPERS), $(call fnTestWrapperTarget,$(wrapper)))
11+
12+
.PHONY: generate $(GENEREATORS)
13+
14+
build: $(GENEREATORS) $(TEST_WRAPPER_TARGETS)
15+
16+
$(GENEREATORS): node_modules
17+
npx ts-node $@.ts > $($@_TARGET)
18+
19+
node_modules: package-lock.json
20+
npm ci
21+
22+
define ruleTestWrapper
23+
$(call fnTestWrapperTarget,$(1)): ../src/$($(1)_BASE_PATH)/$(1).sol
24+
npx ts-node libraryTestWrapper.ts $($(1)_BASE_PATH)/$(1) > $(call fnTestWrapperTarget,$(1))
25+
endef
26+
$(foreach wrapper,$(TEST_WRAPPERS),$(eval $(call ruleTestWrapper,$(wrapper))))
27+
28+

gen/cctpDomains.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { toCapsSnakeCase } from "./utils";
2+
import * as base from "@wormhole-foundation/sdk-base";
3+
4+
const { circleChainId, circleNetworks, circleChainMap } = base.circle;
5+
6+
console.log(
7+
`// SPDX-License-Identifier: Apache 2
8+
pragma solidity ^0.8.0;
9+
10+
// This file is auto-generated by gen/cctpDomains.ts.
11+
`);
12+
13+
if (circleNetworks[0] !== "Mainnet" || circleNetworks[1] !== "Testnet")
14+
throw new Error("circleNetworks has unexpected content");
15+
16+
const mainnetChains = base.column(circleChainMap[0], 0);
17+
const testnetChains = base.column(circleChainMap[1], 0);
18+
19+
for (const chain of mainnetChains)
20+
console.log(
21+
`uint32 constant CCTP_DOMAIN_${toCapsSnakeCase(chain)} = ${circleChainId("Mainnet", chain)};`
22+
);
23+
24+
console.log(`
25+
// Additional Testnet mappings:`);
26+
for (const chain of testnetChains)
27+
if (!(mainnetChains as string[]).includes(chain))
28+
console.log(
29+
`uint32 constant CCTP_DOMAIN_${toCapsSnakeCase(chain)} = ${circleChainId("Testnet", chain)};`
30+
);

gen/chains.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { toCapsSnakeCase } from "./utils";
2+
import * as base from "@wormhole-foundation/sdk-base";
3+
4+
const { chains, chainToChainId } = base.chain;
5+
6+
console.log(
7+
`// SPDX-License-Identifier: Apache 2
8+
pragma solidity ^0.8.0;
9+
10+
// This file is auto-generated by gen/chains.ts.
11+
12+
// In the wormhole wire format, 0 indicates that a message is for any destination chain
13+
uint16 constant CHAIN_ID_UNSET = 0;`);
14+
15+
for (const chain of chains)
16+
console.log(`uint16 constant CHAIN_ID_${toCapsSnakeCase(chain)} = ${chainToChainId(chain)};`);

gen/libraryTestWrapper.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//Simple parser for .sol files that finds libraries defined in the file and creates a wrapper
2+
// that converts all their internal functions to external functions so they can be tested
3+
// in forge unit tests.
4+
//Instead of properly parsing the full AST, we make our task of finding and transforming the
5+
// relevant bits easier by making some basic assumptions about code formatting that any sane
6+
// code should almost certainly adhere to regardless.
7+
8+
//Solidity language grammar:
9+
// https://docs.soliditylang.org/en/latest/grammar.html
10+
11+
import { readFileSync } from "fs";
12+
13+
// const commentTestCode = `
14+
// // /* commented-out block comment start
15+
// code
16+
// // commented-out block comment end */
17+
// const x = 42; // inline comment
18+
// /* block comment // */
19+
// code after comment
20+
// // commented-out block comment end */
21+
// `;
22+
function removeComments(code: string): string {
23+
//?<! is a negative lookbehind assertion:
24+
// it only finds matches that are not preceded by the specified pattern)
25+
//so in our case, we are looking for /* that is not preceded by // to make sure
26+
// that we only delete legitimate block comments
27+
//\S matches any non-whitespace character and used together with \s in [\s\S]
28+
// it matches any character including newlines (when .* would not)
29+
const blockCommentRegex = /(?<!\/\/.*)\/\*[\s\S]*?\*\//g;
30+
//m flag is multiline mode:
31+
// ensures that ^ and $ matches individual line starts/ends rather than the whole string
32+
const lineCommentRegex = /\/\/.*$/gm;
33+
const emptyLineRegex = /(^\s*\n)+/gm;
34+
35+
return code
36+
.replace(blockCommentRegex, '')
37+
.replace(lineCommentRegex, '')
38+
.replace(emptyLineRegex, '\n');
39+
}
40+
41+
if (process.argv.length != 3) {
42+
console.log("requires exactly one command line argument relative to src: <path/libfile[.sol]>");
43+
process.exit(1);
44+
}
45+
46+
const filename = process.argv[2];
47+
let fileCode = readFileSync("../src/" + filename + filename.endsWith(".sol") ? "" : ".sol", "utf8");
48+
49+
//we first remove all comments so we can be sure that everything we're parsing is actual code
50+
fileCode = removeComments(fileCode);
51+
52+
interface Func {
53+
name: string;
54+
stateMut: string;
55+
paras: string[];
56+
rets?: string[];
57+
}
58+
59+
const libraries: Record<string, Func[]> = {};
60+
61+
const libRegex = /library\s+(\w+)\s*\{([\s\S]*?)\n\}/g;
62+
//use library regex to find all libraries in the file and split into name and code pairs
63+
const libMatches = fileCode.matchAll(libRegex);
64+
for (const libs of libMatches) {
65+
const [_, name, code] = libs;
66+
libraries[name] = [];
67+
const structRegex = /\s*struct\s+(\w+)/g;
68+
const structs = new Set<string>();
69+
const structMatches = code.matchAll(structRegex);
70+
for (const struct of structMatches)
71+
structs.add[struct[1]];
72+
73+
const funcRegex = /\s*function\s+(\w+)\s*\(([\s\S]*?)\)([\s\S]*?)([\{;])/g;
74+
const funcMatches = code.matchAll(funcRegex);
75+
for (const funcs of funcMatches) {
76+
const [_, funcName, funcParasRaw, modsRaw, close] = funcs;
77+
if (close == ';')
78+
continue; //function pointer, not a function definition
79+
80+
if (!modsRaw.includes("internal"))
81+
continue; //not an internal function
82+
83+
const retParasRegex = /returns\s*\(([\s\S]*?)\)/;
84+
const retParasRaw = modsRaw.match(retParasRegex);
85+
86+
const collapseSpaceRegex = /\s\s+/g;
87+
const paramsToArray = (paramList: string) =>
88+
paramList.replace(collapseSpaceRegex, ' ').trim().split(',').map(param => {
89+
param = param.trim();
90+
const paraType = param.match(/^(\w+)/)[1];
91+
return structs.has(paraType) ? param.replace(paraType, `${name}.${paraType}`) : param;
92+
});
93+
94+
libraries[name].push({
95+
name: funcName,
96+
stateMut: modsRaw.match(/\b(pure|view)\b/)?.[0] ?? '',
97+
paras: paramsToArray(funcParasRaw.replace(/\bmemory\b/g, ' calldata ')),
98+
rets: retParasRaw ? paramsToArray(retParasRaw[1]) : undefined,
99+
});
100+
}
101+
}
102+
103+
console.log(`// SPDX-License-Identifier: Apache 2
104+
pragma solidity ^0.8.24;
105+
106+
//This file was auto-generated by libraryTestWrapper.ts
107+
108+
import "wormhole-sdk/${filename}.sol";`);
109+
110+
const pConcat = (paras: string[]) =>
111+
(paras.length > 2 ? "\n " : "") +
112+
paras.join(paras.length > 2 ? ",\n " : ", ") +
113+
(paras.length > 2 ? "\n " : "");
114+
115+
const pNames = (paras: string[]) =>
116+
paras.map(para => {
117+
const pieces = para.split(" ");
118+
return pieces[pieces.length-1];
119+
});
120+
121+
122+
for (const [libName, funcs] of Object.entries(libraries)) {
123+
console.log(`\ncontract ${libName}TestWrapper {`);
124+
const funcCode = [];
125+
for (const func of funcs)
126+
funcCode.push([
127+
` function ${func.name}(${pConcat(func.paras)}) external ${func.stateMut}` +
128+
` ${func.rets ? `returns (${pConcat(func.rets)}) ` : ''} {`,
129+
` ${func.rets ? 'return ' : ''}${libName}.${func.name}(${pNames(func.paras).join(', ')});`,
130+
` }`
131+
].join('\n'));
132+
console.log(funcCode.join('\n\n'));
133+
console.log('}');
134+
}

0 commit comments

Comments
 (0)