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
10 changes: 10 additions & 0 deletions .github/workflows/app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: app
on: pull_request
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # pinning the commit to v2
- run: bun install --frozen-lockfile
- run: bun run check
14 changes: 13 additions & 1 deletion bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
"@types/express": "^5.0.3",
"@wormhole-foundation/sdk-base": "^2.4.0",
"@wormhole-foundation/sdk-definitions": "^2.4.0",
"binary-layout": "^1.3.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"viem": "^2.31.7",
},
"devDependencies": {
"@types/bun": "latest",
"@types/cors": "^2.8.17",
"prettier": "^3.5.3",
},
"peerDependencies": {
"typescript": "^5",
Expand Down Expand Up @@ -39,6 +43,8 @@

"@types/connect": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],

"@types/cors": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],

"@types/express": ["@types/[email protected]", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="],

"@types/express-serve-static-core": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="],
Expand All @@ -47,7 +53,7 @@

"@types/mime": ["@types/[email protected]", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],

"@types/node": ["@types/[email protected].11", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-CJV8eqrYnwQJGMrvcRhQmZfpyniDavB+7nAZYJc6w99hFYJyFN3INV1/2W3QfQrqM36WTLrijJ1fxxvGBmCSxA=="],
"@types/node": ["@types/[email protected].12", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g=="],

"@types/qs": ["@types/[email protected]", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],

Expand Down Expand Up @@ -87,6 +93,8 @@

"cookie-signature": ["[email protected]", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],

"cors": ["[email protected]", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],

"csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],

"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
Expand Down Expand Up @@ -157,6 +165,8 @@

"negotiator": ["[email protected]", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],

"object-assign": ["[email protected]", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],

"object-inspect": ["[email protected]", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],

"on-finished": ["[email protected]", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
Expand All @@ -169,6 +179,8 @@

"path-to-regexp": ["[email protected]", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],

"prettier": ["[email protected]", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],

"proxy-addr": ["[email protected]", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],

"qs": ["[email protected]", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
Expand Down
14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,27 @@
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
"@types/bun": "latest",
"@types/cors": "^2.8.17",
"prettier": "^3.5.3"
},
"peerDependencies": {
"typescript": "^5"
},
"scripts": {
"start": "bun src/index.ts"
"start": "bun src/index.ts",
"check": "bun run type:check && bun run prettier:check",
"type:check": "tsc --noEmit",
"prettier:check": "prettier . --check"
},
"dependencies": {
"@types/express": "^5.0.3",
"@wormhole-foundation/sdk-base": "^2.4.0",
"@wormhole-foundation/sdk-definitions": "^2.4.0",
"binary-layout": "^1.3.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"viem": "^2.31.7"
}
},
"prettier": {}
}
17 changes: 17 additions & 0 deletions src/api/capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type Request, type Response } from "express";
import { enabledChains } from "../chains";

export const capabilitiesHandler = async (req: Request, res: Response) => {
const capabilities: Record<string, any> = {};

for (const [_, chainConfig] of Object.entries(enabledChains)) {
capabilities[chainConfig.wormholeChainId.toString()] = {
requestPrefixes: chainConfig.capabilities.requestPrefixes,
gasDropOffLimit: chainConfig.capabilities.gasDropOffLimit.toString(),
maxGasLimit: chainConfig.capabilities.maxGasLimit.toString(),
maxMsgValue: chainConfig.capabilities.maxMsgValue.toString(),
};
}

res.json(capabilities);
};
3 changes: 3 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { quoteHandler } from "./quote";
export { statusHandler } from "./status";
export { capabilitiesHandler } from "./capabilities";
143 changes: 143 additions & 0 deletions src/api/quote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { type Request, type Response } from "express";
import { enabledChains, type ChainConfig } from "../chains";
import { createPublicClient, http, isHex, padHex, toBytes } from "viem";
import type { Quote } from "@wormhole-foundation/sdk-definitions";
import {
PAYEE_PUBLIC_KEY,
QUOTER_PRIVATE_KEY,
QUOTER_PUBLIC_KEY,
} from "../consts";
import {
estimateQuote,
getTotalGasLimitAndMsgValue,
signQuote,
} from "../utils";
import { anvil } from "viem/chains";

function getChainConfig(chainId: string): ChainConfig | undefined {
const numericId = parseInt(chainId);
return enabledChains[numericId];
}

async function getGasPrice(chainConfig: ChainConfig): Promise<bigint> {
try {
const transport = http(chainConfig.rpc);
const client = createPublicClient({
chain: anvil,
transport,
});
return await client.getGasPrice();
} catch (e) {
throw new Error(`unable to determine gas price`);
}
}

export const quoteHandler = async (req: Request, res: Response) => {
const enabledChainIds = Object.keys(enabledChains);

const srcChainId = req.body.srcChain;
const dstChainId = req.body.dstChain;
const relayInstructions = req.body.relayInstructions;

if (relayInstructions && !isHex(relayInstructions)) {
res.status(400).send(`Invalid hex string for "relayInstructions"`);
return;
}

if (!enabledChainIds.includes(srcChainId.toString())) {
res
.status(400)
.send(
`Unsupported source chain: ${srcChainId}, supported chains: ${enabledChainIds.join(
",",
)}`,
);
return;
}

if (!enabledChainIds.includes(dstChainId.toString())) {
res
.status(400)
.send(
`Unsupported destination chain: ${dstChainId}, supported chains: ${enabledChainIds.join(
",",
)}`,
);
return;
}

const srcChain = getChainConfig(srcChainId);
const dstChain = getChainConfig(dstChainId);

if (!srcChain || !dstChain) {
res.status(500).send("Internal error: Invalid chain configuration");
return;
}

const expiryTime = new Date();
expiryTime.setHours(expiryTime.getHours() + 1);

const dstGasPrice = await getGasPrice(dstChain);

const quote: Quote = {
quote: {
prefix: "EQ01",
quoterAddress: toBytes(QUOTER_PUBLIC_KEY),
payeeAddress: toBytes(
padHex(PAYEE_PUBLIC_KEY, {
dir: "left",
size: 32,
}),
),
srcChain: parseInt(srcChainId),
dstChain: parseInt(dstChainId),
expiryTime,
baseFee: 1n,
dstGasPrice: dstGasPrice,
srcPrice: 10000000000n,
dstPrice: 10000000000n,
},
};

const signedQuote = await signQuote(quote, QUOTER_PRIVATE_KEY);

let response: {
signedQuote: `0x${string}`;
estimatedCost?: bigint;
} = {
signedQuote,
};

if (relayInstructions) {
const { gasLimit, msgValue } =
getTotalGasLimitAndMsgValue(relayInstructions);

if (gasLimit > dstChain.capabilities.maxGasLimit) {
res
.status(400)
.send(
`Request exceeds maxGasLimit: ${gasLimit.toString()} requested, ${dstChain.capabilities.maxGasLimit.toString()} maximum.`,
);
return;
}

if (msgValue > dstChain.capabilities.maxMsgValue) {
res
.status(400)
.send(
`Request exceeds maxMsgValue: ${msgValue.toString()} requested, ${dstChain.capabilities.maxMsgValue.toString()} maximum.`,
);
return;
}

response.estimatedCost = estimateQuote(
quote,
gasLimit,
msgValue,
dstChain.gasPriceDecimals,
srcChain.nativeDecimals,
dstChain.nativeDecimals,
);
}
res.status(200).json(response);
};
5 changes: 5 additions & 0 deletions src/api/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { type Request, type Response } from "express";

export const statusHandler = async (req: Request, res: Response) => {
res.status(500).send();
};
42 changes: 42 additions & 0 deletions src/chains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { RequestPrefix, type Capabilities } from "./types";

export interface ChainConfig {
wormholeChainId: number;
evmChainId: number;
rpc: string;
name: string;
gasPriceDecimals: number;
nativeDecimals: number;
capabilities: Capabilities;
}

export const enabledChains: Record<number, ChainConfig> = {
10002: {
wormholeChainId: 10002,
evmChainId: 11155111,
rpc: "http://anvil-eth-sepolia:8545",
name: "Ethereum Sepolia",
gasPriceDecimals: 18,
nativeDecimals: 18,
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,
capabilities: {
requestPrefixes: [RequestPrefix.ERV1],
gasDropOffLimit: 100_000_000_000n,
maxGasLimit: 1_000_000n,
maxMsgValue: 100_000_000_000n * 2n,
},
},
};
7 changes: 6 additions & 1 deletion src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import { mnemonicToAccount } from "viem/accounts";

const account = mnemonicToAccount(
"test test test test test test test test test test test junk",
{ addressIndex: 9 }
{ addressIndex: 9 },
);

export const PAYEE_PUBLIC_KEY = account.address;

export const EVM_PUBLIC_KEY = account.address;
export const EVM_PRIVATE_KEY = toHex(account.getHdKey().privateKey || "0x");

export const QUOTER_PUBLIC_KEY = account.address;
export const QUOTER_PRIVATE_KEY = toHex(account.getHdKey().privateKey || "0x");
29 changes: 17 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import express, { type Request, type Response } from "express";
import express from "express";
import cors from "cors";
import { overrideGuardianSet } from "./overrideGuardianSet";
import { quoteHandler, statusHandler, capabilitiesHandler } from "./api";

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

await overrideGuardianSet(
"http://anvil-eth-sepolia:8545",
"0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78"
"0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78",
);
await overrideGuardianSet(
"http://anvil-base-sepolia:8545",
"0x79A1027a6A159502049F10906D333EC57E95F083"
"0x79A1027a6A159502049F10906D333EC57E95F083",
);

const app = express();

app.use(cors());
app.use(express.json());
app.post("/v0/quote", async (req: Request, res: Response) => {
res.status(500).send();
});
app.post("/v0/status/tx", async (req: Request, res: Response) => {
res.status(500).send();
});
app.get("/v0/capabilities", async (req: Request, res: Response) => {
res.status(500).send();
});
app.post("/v0/quote", quoteHandler);
app.post("/v0/status/tx", statusHandler);
app.get("/v0/capabilities", capabilitiesHandler);

const server = app.listen(3000, () => {
console.log(`Server is running at http://localhost:3000`);
});
Expand Down
Loading
Loading