Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ FROM base AS release
COPY --from=install /temp/prod/node_modules node_modules
COPY package.json package.json
COPY src src
COPY chains.example.json /config/chains.json

ENV CHAIN_CONFIG_PATH=/config/chains.json

USER bun
EXPOSE 3000/tcp
Expand Down
2 changes: 1 addition & 1 deletion Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ docker_build(
dockerfile = "./Dockerfile",
target="test",
only=[
"package.json", "bun.lock", "src", ".env.test"
"package.json", "bun.lock", "src", ".env.test", "chains.example.json"
]
)

Expand Down
40 changes: 40 additions & 0 deletions chains.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"10002": {
"wormholeChainId": 10002,
"evmChainId": 11155111,
"rpc": "http://anvil-eth-sepolia:8545",
"name": "Ethereum Sepolia",
"viemChainName": "sepolia",
"viemTokenName": "Ether",
"viemTokenSymbol": "ETH",
"gasPriceDecimals": 18,
"nativeDecimals": 18,
"executorAddress": "0xD0fb39f5a3361F21457653cB70F9D0C9bD86B66B",
"coreContractAddress": "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78",
"capabilities": {
"requestPrefixes": ["ERV1"],
"gasDropOffLimit": "100000000000",
"maxGasLimit": "1000000",
"maxMsgValue": "200000000000"
}
},
"10004": {
"wormholeChainId": 10004,
"evmChainId": 84532,
"rpc": "http://anvil-base-sepolia:8545",
"name": "Base Sepolia",
"viemChainName": "baseSepolia",
"viemTokenName": "Ether",
"viemTokenSymbol": "ETH",
"gasPriceDecimals": 18,
"nativeDecimals": 18,
"coreContractAddress": "0x79A1027a6A159502049F10906D333EC57E95F083",
"executorAddress": "0x51B47D493CBA7aB97e3F8F163D6Ce07592CE4482",
"capabilities": {
"requestPrefixes": ["ERV1"],
"gasDropOffLimit": "100000000000",
"maxGasLimit": "1000000",
"maxMsgValue": "200000000000"
}
}
}
149 changes: 149 additions & 0 deletions src/chains.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { beforeEach, describe, expect, test, mock, spyOn } from "bun:test";
import { existsSync, readFileSync } from "fs";
import { RequestPrefix } from "./types";
import { chainToChainId } from "@wormhole-foundation/sdk-base";

const mockExistsSync = mock(existsSync);
const mockReadFileSync = mock(readFileSync);

mock.module("fs", () => ({
existsSync: mockExistsSync,
readFileSync: mockReadFileSync,
}));

describe("Custom Chain Loading", () => {
beforeEach(() => {
mockExistsSync.mockClear();
mockReadFileSync.mockClear();

delete process.env.CHAIN_CONFIG_PATH;
delete process.env.CUSTOM_CHAINS;
});

test("loads custom chains from file when file exists", async () => {
const mockCustomChainData = {
"10003": {
wormholeChainId: 10003,
evmChainId: 421614,
rpc: "https://sepolia-rollup.arbitrum.io/rpc",
name: "Arbitrum Sepolia",
gasPriceDecimals: 18,
nativeDecimals: 18,
executorAddress: "0x1234567890123456789012345678901234567890",
coreContractAddress: "0x0987654321098765432109876543210987654321",
viemChainName: "arbitrumSepolia",
viemTokenName: "Ether",
viemTokenSymbol: "ETH",
capabilities: {
requestPrefixes: ["ERV1"],
gasDropOffLimit: "100000000000",
maxGasLimit: "1000000",
maxMsgValue: "200000000000",
},
},
};

mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(JSON.stringify(mockCustomChainData));

delete require.cache[require.resolve("./chains")];
const { enabledChains } = await import("./chains");

expect(mockExistsSync).toHaveBeenCalledWith("/config/chains.json");
expect(mockReadFileSync).toHaveBeenCalledWith(
"/config/chains.json",
"utf-8",
);

expect(enabledChains[10003]).toBeDefined();
expect(enabledChains[10003]!.name).toBe("Arbitrum Sepolia");
expect(enabledChains[10003]!.evmChainId).toBe(421614);

expect(enabledChains[10003]!.capabilities.gasDropOffLimit).toBe(
100000000000n,
);
expect(enabledChains[10003]!.capabilities.maxGasLimit).toBe(1000000n);
expect(enabledChains[10003]!.capabilities.maxMsgValue).toBe(200000000000n);
expect(enabledChains[10003]!.capabilities.requestPrefixes).toEqual([
RequestPrefix.ERV1,
]);

expect(enabledChains[10003]!.viemChain).toBeDefined();
expect(enabledChains[10003]!.viemChain?.name).toBe("arbitrumSepolia");
expect(enabledChains[10003]!.viemChain?.id).toBe(421614);
expect(enabledChains[10003]!.viemChain?.nativeCurrency.name).toBe("Ether");
expect(enabledChains[10003]!.viemChain?.nativeCurrency.symbol).toBe("ETH");
});

test("loads custom chains from custom path when CHAIN_CONFIG_PATH is set", async () => {
const customPath = "/custom/path/chains.json";
process.env.CHAIN_CONFIG_PATH = customPath;

chainToChainId("ArbitrumSepolia");
const mockCustomChainData = {
"10003": {
wormholeChainId: 10003,
evmChainId: 1,
rpc: "https://eth-mainnet.example.com",
name: "Ethereum Mainnet",
gasPriceDecimals: 18,
nativeDecimals: 18,
executorAddress: "0x1111111111111111111111111111111111111111",
coreContractAddress: "0x2222222222222222222222222222222222222222",
viemChainName: "mainnet",
viemTokenName: "Ether",
viemTokenSymbol: "ETH",
capabilities: {
requestPrefixes: ["ERV1"],
gasDropOffLimit: "50000000000",
maxGasLimit: "2000000",
maxMsgValue: "100000000000",
},
},
};

mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(JSON.stringify(mockCustomChainData));

delete require.cache[require.resolve("./chains")];
const { enabledChains } = await import("./chains");

expect(mockExistsSync).toHaveBeenCalledWith(customPath);
expect(mockReadFileSync).toHaveBeenCalledWith(customPath, "utf-8");
expect(enabledChains[10003]).toBeDefined();
expect(enabledChains[10003]!.name).toBe("Ethereum Mainnet");
});

test("handles file reading errors gracefully", async () => {
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});

mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation(() => {
throw new Error("File read error");
});

delete require.cache[require.resolve("./chains")];
const { enabledChains } = await import("./chains");

expect(consoleSpy).toHaveBeenCalledWith(
"Failed to load custom chains from",
"/config/chains.json",
expect.any(Error),
);
});

test("handles invalid JSON in file gracefully", async () => {
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});

mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue("invalid json content");

delete require.cache[require.resolve("./chains")];

expect(consoleSpy).toHaveBeenCalledWith(
"Failed to load custom chains from",
"/config/chains.json",
expect.any(Error),
);
});
});
96 changes: 59 additions & 37 deletions src/chains.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Chain } from "viem";
import { RequestPrefix, type Capabilities } from "./types";
import { baseSepolia, sepolia } from "viem/chains";
import { defineChain, type Chain } from "viem";
import { type Capabilities } from "./types";
import { readFileSync, existsSync } from "fs";

export interface ChainConfig {
wormholeChainId: number;
Expand All @@ -15,39 +15,61 @@ export interface ChainConfig {
viemChain?: Chain;
}

export interface CustomChainConfig extends Omit<ChainConfig, "viemChain"> {
viemChainName: string;
viemTokenName: string;
viemTokenSymbol: string;
}

function loadChainsConfig(): Record<number, ChainConfig> {
const chains: Record<number, ChainConfig> = {};

const configPath = process.env.CHAIN_CONFIG_PATH || "/config/chains.json";

if (existsSync(configPath)) {
try {
const fileContent = readFileSync(configPath, "utf-8");
const customChainsData = JSON.parse(fileContent) as Record<
string,
CustomChainConfig
>;

for (const [chainId, config] of Object.entries(customChainsData)) {
const capabilities = {
...config.capabilities,
gasDropOffLimit: BigInt(config.capabilities.gasDropOffLimit),
maxGasLimit: BigInt(config.capabilities.maxGasLimit),
maxMsgValue: BigInt(config.capabilities.maxMsgValue),
};

const chainConfig: ChainConfig = {
...config,
capabilities,
viemChain: defineChain({
id: config.evmChainId,
nativeCurrency: {
decimals: config.nativeDecimals,
name: config.viemTokenName,
symbol: config.viemTokenSymbol,
},
name: config.viemChainName,
rpcUrls: {
default: {
http: [config.rpc],
},
},
}),
};
chains[Number(chainId)] = chainConfig;
}
} catch (error) {
console.error("Failed to load custom chains from", configPath, error);
}
}

return chains;
}

export const enabledChains: Record<number, ChainConfig> = {
10002: {
wormholeChainId: 10002,
evmChainId: 11155111,
rpc: "http://anvil-eth-sepolia:8545",
name: "Ethereum Sepolia",
gasPriceDecimals: 18,
nativeDecimals: 18,
executorAddress: "0xD0fb39f5a3361F21457653cB70F9D0C9bD86B66B",
coreContractAddress: "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78",
viemChain: sepolia,
capabilities: {
requestPrefixes: [RequestPrefix.ERV1],
gasDropOffLimit: 100_000_000_000n,
maxGasLimit: 1_000_000n,
maxMsgValue: 100_000_000_000n * 2n,
},
},
10004: {
wormholeChainId: 10004,
evmChainId: 84532,
rpc: "http://anvil-base-sepolia:8545",
name: "Base Sepolia",
gasPriceDecimals: 18,
nativeDecimals: 18,
viemChain: baseSepolia,
coreContractAddress: "0x79A1027a6A159502049F10906D333EC57E95F083",
executorAddress: "0x51B47D493CBA7aB97e3F8F163D6Ce07592CE4482",
capabilities: {
requestPrefixes: [RequestPrefix.ERV1],
gasDropOffLimit: 100_000_000_000n,
maxGasLimit: 1_000_000n,
maxMsgValue: 100_000_000_000n * 2n,
},
},
...loadChainsConfig(),
};
18 changes: 9 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import express from "express";
import cors from "cors";
import { overrideGuardianSet } from "./overrideGuardianSet";
import { quoteHandler, statusHandler, capabilitiesHandler } from "./api";
import { enabledChains } from "./chains";
import { isHex } from "viem";

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

await overrideGuardianSet(
"http://anvil-eth-sepolia:8545",
"0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78",
);
await overrideGuardianSet(
"http://anvil-base-sepolia:8545",
"0x79A1027a6A159502049F10906D333EC57E95F083",
);
for (const chain of Object.values(enabledChains)) {
if (!isHex(chain.coreContractAddress)) {
throw new Error(`Invalid hex address for wormhole core contract`);
}

await overrideGuardianSet(chain.rpc, chain.coreContractAddress);
}

const app = express();

Expand Down
Loading