Skip to content

Commit 873ac2b

Browse files
douglasgalicoevan-gray
authored andcommitted
dynamic file loading
1 parent 7044510 commit 873ac2b

File tree

6 files changed

+261
-47
lines changed

6 files changed

+261
-47
lines changed

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ FROM base AS release
1212
COPY --from=install /temp/prod/node_modules node_modules
1313
COPY package.json package.json
1414
COPY src src
15+
COPY chains.example.json /config/chains.json
16+
17+
ENV CHAIN_CONFIG_PATH=/config/chains.json
1518

1619
USER bun
1720
EXPOSE 3000/tcp

Tiltfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ docker_build(
2121
dockerfile = "./Dockerfile",
2222
target="test",
2323
only=[
24-
"package.json", "bun.lock", "src", ".env.test"
24+
"package.json", "bun.lock", "src", ".env.test", "chains.example.json"
2525
]
2626
)
2727

chains.example.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"10002": {
3+
"wormholeChainId": 10002,
4+
"evmChainId": 11155111,
5+
"rpc": "http://anvil-eth-sepolia:8545",
6+
"name": "Ethereum Sepolia",
7+
"viemChainName": "sepolia",
8+
"viemTokenName": "Ether",
9+
"viemTokenSymbol": "ETH",
10+
"gasPriceDecimals": 18,
11+
"nativeDecimals": 18,
12+
"executorAddress": "0xD0fb39f5a3361F21457653cB70F9D0C9bD86B66B",
13+
"coreContractAddress": "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78",
14+
"capabilities": {
15+
"requestPrefixes": ["ERV1"],
16+
"gasDropOffLimit": "100000000000",
17+
"maxGasLimit": "1000000",
18+
"maxMsgValue": "200000000000"
19+
}
20+
},
21+
"10004": {
22+
"wormholeChainId": 10004,
23+
"evmChainId": 84532,
24+
"rpc": "http://anvil-base-sepolia:8545",
25+
"name": "Base Sepolia",
26+
"viemChainName": "baseSepolia",
27+
"viemTokenName": "Ether",
28+
"viemTokenSymbol": "ETH",
29+
"gasPriceDecimals": 18,
30+
"nativeDecimals": 18,
31+
"coreContractAddress": "0x79A1027a6A159502049F10906D333EC57E95F083",
32+
"executorAddress": "0x51B47D493CBA7aB97e3F8F163D6Ce07592CE4482",
33+
"capabilities": {
34+
"requestPrefixes": ["ERV1"],
35+
"gasDropOffLimit": "100000000000",
36+
"maxGasLimit": "1000000",
37+
"maxMsgValue": "200000000000"
38+
}
39+
}
40+
}

src/chains.test.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { beforeEach, describe, expect, test, mock, spyOn } from "bun:test";
2+
import { existsSync, readFileSync } from "fs";
3+
import { RequestPrefix } from "./types";
4+
import { chainToChainId } from "@wormhole-foundation/sdk-base";
5+
6+
const mockExistsSync = mock(existsSync);
7+
const mockReadFileSync = mock(readFileSync);
8+
9+
mock.module("fs", () => ({
10+
existsSync: mockExistsSync,
11+
readFileSync: mockReadFileSync,
12+
}));
13+
14+
describe("Custom Chain Loading", () => {
15+
beforeEach(() => {
16+
mockExistsSync.mockClear();
17+
mockReadFileSync.mockClear();
18+
19+
delete process.env.CHAIN_CONFIG_PATH;
20+
delete process.env.CUSTOM_CHAINS;
21+
});
22+
23+
test("loads custom chains from file when file exists", async () => {
24+
const mockCustomChainData = {
25+
"10003": {
26+
wormholeChainId: 10003,
27+
evmChainId: 421614,
28+
rpc: "https://sepolia-rollup.arbitrum.io/rpc",
29+
name: "Arbitrum Sepolia",
30+
gasPriceDecimals: 18,
31+
nativeDecimals: 18,
32+
executorAddress: "0x1234567890123456789012345678901234567890",
33+
coreContractAddress: "0x0987654321098765432109876543210987654321",
34+
viemChainName: "arbitrumSepolia",
35+
viemTokenName: "Ether",
36+
viemTokenSymbol: "ETH",
37+
capabilities: {
38+
requestPrefixes: ["ERV1"],
39+
gasDropOffLimit: "100000000000",
40+
maxGasLimit: "1000000",
41+
maxMsgValue: "200000000000",
42+
},
43+
},
44+
};
45+
46+
mockExistsSync.mockReturnValue(true);
47+
mockReadFileSync.mockReturnValue(JSON.stringify(mockCustomChainData));
48+
49+
delete require.cache[require.resolve("./chains")];
50+
const { enabledChains } = await import("./chains");
51+
52+
expect(mockExistsSync).toHaveBeenCalledWith("/config/chains.json");
53+
expect(mockReadFileSync).toHaveBeenCalledWith(
54+
"/config/chains.json",
55+
"utf-8",
56+
);
57+
58+
expect(enabledChains[10003]).toBeDefined();
59+
expect(enabledChains[10003]!.name).toBe("Arbitrum Sepolia");
60+
expect(enabledChains[10003]!.evmChainId).toBe(421614);
61+
62+
expect(enabledChains[10003]!.capabilities.gasDropOffLimit).toBe(
63+
100000000000n,
64+
);
65+
expect(enabledChains[10003]!.capabilities.maxGasLimit).toBe(1000000n);
66+
expect(enabledChains[10003]!.capabilities.maxMsgValue).toBe(200000000000n);
67+
expect(enabledChains[10003]!.capabilities.requestPrefixes).toEqual([
68+
RequestPrefix.ERV1,
69+
]);
70+
71+
expect(enabledChains[10003]!.viemChain).toBeDefined();
72+
expect(enabledChains[10003]!.viemChain?.name).toBe("arbitrumSepolia");
73+
expect(enabledChains[10003]!.viemChain?.id).toBe(421614);
74+
expect(enabledChains[10003]!.viemChain?.nativeCurrency.name).toBe("Ether");
75+
expect(enabledChains[10003]!.viemChain?.nativeCurrency.symbol).toBe("ETH");
76+
});
77+
78+
test("loads custom chains from custom path when CHAIN_CONFIG_PATH is set", async () => {
79+
const customPath = "/custom/path/chains.json";
80+
process.env.CHAIN_CONFIG_PATH = customPath;
81+
82+
chainToChainId("ArbitrumSepolia");
83+
const mockCustomChainData = {
84+
"10003": {
85+
wormholeChainId: 10003,
86+
evmChainId: 1,
87+
rpc: "https://eth-mainnet.example.com",
88+
name: "Ethereum Mainnet",
89+
gasPriceDecimals: 18,
90+
nativeDecimals: 18,
91+
executorAddress: "0x1111111111111111111111111111111111111111",
92+
coreContractAddress: "0x2222222222222222222222222222222222222222",
93+
viemChainName: "mainnet",
94+
viemTokenName: "Ether",
95+
viemTokenSymbol: "ETH",
96+
capabilities: {
97+
requestPrefixes: ["ERV1"],
98+
gasDropOffLimit: "50000000000",
99+
maxGasLimit: "2000000",
100+
maxMsgValue: "100000000000",
101+
},
102+
},
103+
};
104+
105+
mockExistsSync.mockReturnValue(true);
106+
mockReadFileSync.mockReturnValue(JSON.stringify(mockCustomChainData));
107+
108+
delete require.cache[require.resolve("./chains")];
109+
const { enabledChains } = await import("./chains");
110+
111+
expect(mockExistsSync).toHaveBeenCalledWith(customPath);
112+
expect(mockReadFileSync).toHaveBeenCalledWith(customPath, "utf-8");
113+
expect(enabledChains[10003]).toBeDefined();
114+
expect(enabledChains[10003]!.name).toBe("Ethereum Mainnet");
115+
});
116+
117+
test("handles file reading errors gracefully", async () => {
118+
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
119+
120+
mockExistsSync.mockReturnValue(true);
121+
mockReadFileSync.mockImplementation(() => {
122+
throw new Error("File read error");
123+
});
124+
125+
delete require.cache[require.resolve("./chains")];
126+
const { enabledChains } = await import("./chains");
127+
128+
expect(consoleSpy).toHaveBeenCalledWith(
129+
"Failed to load custom chains from",
130+
"/config/chains.json",
131+
expect.any(Error),
132+
);
133+
});
134+
135+
test("handles invalid JSON in file gracefully", async () => {
136+
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
137+
138+
mockExistsSync.mockReturnValue(true);
139+
mockReadFileSync.mockReturnValue("invalid json content");
140+
141+
delete require.cache[require.resolve("./chains")];
142+
143+
expect(consoleSpy).toHaveBeenCalledWith(
144+
"Failed to load custom chains from",
145+
"/config/chains.json",
146+
expect.any(Error),
147+
);
148+
});
149+
});

src/chains.ts

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { Chain } from "viem";
2-
import { RequestPrefix, type Capabilities } from "./types";
3-
import { baseSepolia, sepolia } from "viem/chains";
1+
import { defineChain, type Chain } from "viem";
2+
import { type Capabilities } from "./types";
3+
import { readFileSync, existsSync } from "fs";
44

55
export interface ChainConfig {
66
wormholeChainId: number;
@@ -15,39 +15,61 @@ export interface ChainConfig {
1515
viemChain?: Chain;
1616
}
1717

18+
export interface CustomChainConfig extends Omit<ChainConfig, "viemChain"> {
19+
viemChainName: string;
20+
viemTokenName: string;
21+
viemTokenSymbol: string;
22+
}
23+
24+
function loadChainsConfig(): Record<number, ChainConfig> {
25+
const chains: Record<number, ChainConfig> = {};
26+
27+
const configPath = process.env.CHAIN_CONFIG_PATH || "/config/chains.json";
28+
29+
if (existsSync(configPath)) {
30+
try {
31+
const fileContent = readFileSync(configPath, "utf-8");
32+
const customChainsData = JSON.parse(fileContent) as Record<
33+
string,
34+
CustomChainConfig
35+
>;
36+
37+
for (const [chainId, config] of Object.entries(customChainsData)) {
38+
const capabilities = {
39+
...config.capabilities,
40+
gasDropOffLimit: BigInt(config.capabilities.gasDropOffLimit),
41+
maxGasLimit: BigInt(config.capabilities.maxGasLimit),
42+
maxMsgValue: BigInt(config.capabilities.maxMsgValue),
43+
};
44+
45+
const chainConfig: ChainConfig = {
46+
...config,
47+
capabilities,
48+
viemChain: defineChain({
49+
id: config.evmChainId,
50+
nativeCurrency: {
51+
decimals: config.nativeDecimals,
52+
name: config.viemTokenName,
53+
symbol: config.viemTokenSymbol,
54+
},
55+
name: config.viemChainName,
56+
rpcUrls: {
57+
default: {
58+
http: [config.rpc],
59+
},
60+
},
61+
}),
62+
};
63+
chains[Number(chainId)] = chainConfig;
64+
}
65+
} catch (error) {
66+
console.error("Failed to load custom chains from", configPath, error);
67+
}
68+
}
69+
70+
return chains;
71+
}
72+
1873
export const enabledChains: Record<number, ChainConfig> = {
19-
10002: {
20-
wormholeChainId: 10002,
21-
evmChainId: 11155111,
22-
rpc: "http://anvil-eth-sepolia:8545",
23-
name: "Ethereum Sepolia",
24-
gasPriceDecimals: 18,
25-
nativeDecimals: 18,
26-
executorAddress: "0xD0fb39f5a3361F21457653cB70F9D0C9bD86B66B",
27-
coreContractAddress: "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78",
28-
viemChain: sepolia,
29-
capabilities: {
30-
requestPrefixes: [RequestPrefix.ERV1],
31-
gasDropOffLimit: 100_000_000_000n,
32-
maxGasLimit: 1_000_000n,
33-
maxMsgValue: 100_000_000_000n * 2n,
34-
},
35-
},
36-
10004: {
37-
wormholeChainId: 10004,
38-
evmChainId: 84532,
39-
rpc: "http://anvil-base-sepolia:8545",
40-
name: "Base Sepolia",
41-
gasPriceDecimals: 18,
42-
nativeDecimals: 18,
43-
viemChain: baseSepolia,
44-
coreContractAddress: "0x79A1027a6A159502049F10906D333EC57E95F083",
45-
executorAddress: "0x51B47D493CBA7aB97e3F8F163D6Ce07592CE4482",
46-
capabilities: {
47-
requestPrefixes: [RequestPrefix.ERV1],
48-
gasDropOffLimit: 100_000_000_000n,
49-
maxGasLimit: 1_000_000n,
50-
maxMsgValue: 100_000_000_000n * 2n,
51-
},
52-
},
74+
...loadChainsConfig(),
5375
};

src/index.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@ import express from "express";
22
import cors from "cors";
33
import { overrideGuardianSet } from "./overrideGuardianSet";
44
import { quoteHandler, statusHandler, capabilitiesHandler } from "./api";
5+
import { enabledChains } from "./chains";
6+
import { isHex } from "viem";
57

68
// @ts-ignore
79
BigInt.prototype.toJSON = function () {
8-
// Can also be JSON.rawJSON(this.toString());
910
return this.toString();
1011
};
1112

12-
await overrideGuardianSet(
13-
"http://anvil-eth-sepolia:8545",
14-
"0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78",
15-
);
16-
await overrideGuardianSet(
17-
"http://anvil-base-sepolia:8545",
18-
"0x79A1027a6A159502049F10906D333EC57E95F083",
19-
);
13+
for (const chain of Object.values(enabledChains)) {
14+
if (!isHex(chain.coreContractAddress)) {
15+
throw new Error(`Invalid hex address for wormhole core contract`);
16+
}
17+
18+
await overrideGuardianSet(chain.rpc, chain.coreContractAddress);
19+
}
2020

2121
const app = express();
2222

0 commit comments

Comments
 (0)