Skip to content

Commit 07c3eda

Browse files
committed
capabilities & quote endpoints
1 parent 655faa4 commit 07c3eda

File tree

11 files changed

+264
-10
lines changed

11 files changed

+264
-10
lines changed

bun.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"name": "example-executor-ci-test",
66
"dependencies": {
77
"@types/express": "^5.0.3",
8+
"@wormhole-foundation/sdk-definitions": "^2.4.0",
9+
"binary-layout": "^1.3.0",
810
"express": "^5.1.0",
911
"viem": "^2.31.7",
1012
},
@@ -57,10 +59,16 @@
5759

5860
"@types/serve-static": ["@types/[email protected]", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
5961

62+
"@wormhole-foundation/sdk-base": ["@wormhole-foundation/[email protected]", "", { "dependencies": { "@scure/base": "^1.1.3", "binary-layout": "^1.0.3" } }, "sha512-8LbhBMeiDgykZSYc3hkV48bUbasYppCTt3CBQltqBGNUO56sr08HLLCde7R1y9/15qmgQv4cijjp0i/0fNu5hA=="],
63+
64+
"@wormhole-foundation/sdk-definitions": ["@wormhole-foundation/[email protected]", "", { "dependencies": { "@noble/curves": "^1.4.0", "@noble/hashes": "^1.3.1", "@wormhole-foundation/sdk-base": "2.4.0" } }, "sha512-Aqx3/XLaBzbt5kt70N0lnVj3acGe/DYN66R4lG7AVv7VvDTSj2PKC0qOdbKgMh+bzFbjKK03fJkpUzl/d6eo+A=="],
65+
6066
"abitype": ["[email protected]", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg=="],
6167

6268
"accepts": ["[email protected]", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
6369

70+
"binary-layout": ["[email protected]", "", {}, "sha512-jDJ6rLgjjQ9q8NP5eIumdvsegbbMsNplJ7GHMuVnMWi0Qw59o8kIOw+ew4fLAryPL3LgIp5KrjfdAMoSpmpO8w=="],
71+
6472
"body-parser": ["[email protected]", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
6573

6674
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
},
1515
"dependencies": {
1616
"@types/express": "^5.0.3",
17+
"@wormhole-foundation/sdk-definitions": "^2.4.0",
18+
"binary-layout": "^1.3.0",
1719
"express": "^5.1.0",
1820
"viem": "^2.31.7"
1921
}

src/api/capabilities.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { type Request, type Response } from "express";
2+
import { enabledChains } from "../chains";
3+
4+
export const capabilitiesHandler = async (req: Request, res: Response) => {
5+
const capabilities: Record<string, any> = {};
6+
7+
for (const [_, chainConfig] of Object.entries(enabledChains)) {
8+
capabilities[chainConfig.wormholeChainId.toString()] = {
9+
requestPrefixes: chainConfig.capabilities.requestPrefixes,
10+
gasDropOffLimit: chainConfig.capabilities.gasDropOffLimit.toString(),
11+
maxGasLimit: chainConfig.capabilities.maxGasLimit.toString(),
12+
maxMsgValue: chainConfig.capabilities.maxMsgValue.toString(),
13+
};
14+
}
15+
16+
res.json(capabilities);
17+
};

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { quoteHandler } from "./quote";
2+
export { statusHandler } from "./status";
3+
export { capabilitiesHandler } from "./capabilities";

src/api/quote.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { type Request, type Response } from "express";
2+
import { enabledChains, type ChainConfig } from "../chains";
3+
import { isHex, padHex, toBytes } from "viem";
4+
import type { Quote } from "@wormhole-foundation/sdk-definitions";
5+
import { QUOTER_PRIVATE_KEY, QUOTER_PUBLIC_KEY } from "../consts";
6+
import { getTotalGasLimitAndMsgValue, signQuote } from "../utils";
7+
8+
function getChainConfig(chainId: string): ChainConfig | undefined {
9+
const numericId = parseInt(chainId);
10+
return enabledChains[numericId];
11+
}
12+
13+
export const quoteHandler = async (req: Request, res: Response) => {
14+
const enabledChainIds = Object.keys(enabledChains);
15+
16+
const srcChainId = req.body.srcChain;
17+
const dstChainId = req.body.dstChain;
18+
const relayInstructions = req.body.relayInstructions;
19+
20+
if (relayInstructions && !isHex(relayInstructions)) {
21+
res.status(400).send(`Invalid hex string for "relayInstructions"`);
22+
return;
23+
}
24+
25+
if (!enabledChainIds.includes(srcChainId.toString())) {
26+
res
27+
.status(400)
28+
.send(
29+
`Unsupported source chain: ${srcChainId}, supported chains: ${enabledChainIds.join(
30+
","
31+
)}`
32+
);
33+
return;
34+
}
35+
36+
if (!enabledChainIds.includes(dstChainId.toString())) {
37+
res
38+
.status(400)
39+
.send(
40+
`Unsupported destination chain: ${dstChainId}, supported chains: ${enabledChainIds.join(
41+
","
42+
)}`
43+
);
44+
return;
45+
}
46+
47+
const srcChain = getChainConfig(srcChainId);
48+
const dstChain = getChainConfig(dstChainId);
49+
50+
if (!srcChain || !dstChain) {
51+
res.status(500).send("Internal error: Invalid chain configuration");
52+
return;
53+
}
54+
55+
const expiryTime = new Date();
56+
57+
expiryTime.setHours(expiryTime.getHours() + 1);
58+
59+
const quote: Quote = {
60+
quote: {
61+
prefix: "EQ01",
62+
quoterAddress: toBytes(QUOTER_PUBLIC_KEY),
63+
payeeAddress: toBytes(
64+
padHex("0x000000000000000000", {
65+
dir: "left",
66+
size: 32,
67+
})
68+
),
69+
srcChain: parseInt(srcChainId),
70+
dstChain: parseInt(dstChainId),
71+
expiryTime,
72+
baseFee: 1n,
73+
dstGasPrice: 100n,
74+
srcPrice: 1n,
75+
dstPrice: 1n,
76+
},
77+
};
78+
79+
const signedQuote = await signQuote(quote, QUOTER_PRIVATE_KEY);
80+
81+
const response = {
82+
signedQuote,
83+
};
84+
85+
if (relayInstructions) {
86+
const { gasLimit, msgValue } =
87+
getTotalGasLimitAndMsgValue(relayInstructions);
88+
89+
if (gasLimit > dstChain.capabilities.maxGasLimit) {
90+
res
91+
.status(400)
92+
.send(
93+
`Request exceeds maxGasLimit: ${gasLimit.toString()} requested, ${dstChain.capabilities.maxGasLimit.toString()} maximum.`
94+
);
95+
return;
96+
}
97+
98+
if (msgValue > dstChain.capabilities.maxMsgValue) {
99+
res
100+
.status(400)
101+
.send(
102+
`Request exceeds maxMsgValue: ${msgValue.toString()} requested, ${dstChain.capabilities.maxMsgValue.toString()} maximum.`
103+
);
104+
return;
105+
}
106+
}
107+
res.status(200).json(response);
108+
};

src/api/status.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { type Request, type Response } from "express";
2+
3+
export const statusHandler = async (req: Request, res: Response) => {
4+
res.status(500).send();
5+
};

src/chains.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { RequestPrefix, type Capabilities } from "./types";
2+
3+
export interface ChainConfig {
4+
wormholeChainId: number;
5+
evmChainId: number;
6+
rpc: string;
7+
name: string;
8+
capabilities: Capabilities;
9+
}
10+
11+
export const enabledChains: Record<number, ChainConfig> = {
12+
10002: {
13+
wormholeChainId: 10002,
14+
evmChainId: 11155111,
15+
rpc: "http://anvil-eth-sepolia:8545",
16+
name: "Ethereum Sepolia",
17+
capabilities: {
18+
requestPrefixes: [RequestPrefix.ERV1],
19+
gasDropOffLimit: 100_000_000_000n,
20+
maxGasLimit: 1_000_000n,
21+
maxMsgValue: 100_000_000_000n * 2n,
22+
},
23+
},
24+
10004: {
25+
wormholeChainId: 10004,
26+
evmChainId: 84532,
27+
rpc: "http://anvil-base-sepolia:8545",
28+
name: "Base 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+
};

src/consts.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { toHex } from "viem";
2+
import { mnemonicToAccount } from "viem/accounts";
3+
4+
const quoterAccount = mnemonicToAccount(
5+
"test test test test test test test test test test test junk",
6+
{ addressIndex: 8 }
7+
);
8+
9+
export const QUOTER_PUBLIC_KEY = quoterAccount.address;
10+
export const QUOTER_PRIVATE_KEY = toHex(
11+
quoterAccount.getHdKey().privateKey || "0x"
12+
);

src/index.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import express, { type Request, type Response } from "express";
1+
import express from "express";
22
import { overrideGuardianSet } from "./overrideGuardianSet";
3+
import { quoteHandler, statusHandler, capabilitiesHandler } from "./api";
34

45
await overrideGuardianSet(
56
"http://anvil-eth-sepolia:8545",
@@ -11,16 +12,12 @@ await overrideGuardianSet(
1112
);
1213

1314
const app = express();
15+
1416
app.use(express.json());
15-
app.post("/v0/quote", async (req: Request, res: Response) => {
16-
res.status(500).send();
17-
});
18-
app.post("/v0/status/tx", async (req: Request, res: Response) => {
19-
res.status(500).send();
20-
});
21-
app.get("/v0/capabilities", async (req: Request, res: Response) => {
22-
res.status(500).send();
23-
});
17+
app.post("/v0/quote", quoteHandler);
18+
app.post("/v0/status/tx", statusHandler);
19+
app.get("/v0/capabilities", capabilitiesHandler);
20+
2421
const server = app.listen(3000, () => {
2522
console.log(`Server is running at http://localhost:3000`);
2623
});

src/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export enum RequestPrefix {
2+
ERM1 = "ERM1", // MM
3+
ERV1 = "ERV1", // VAA_V1
4+
ERN1 = "ERN1", // NTT_V1
5+
ERC1 = "ERC1", // CCTP_V1
6+
ERC2 = "ERC2", // CCTP_V2
7+
}
8+
9+
export type Capabilities = {
10+
requestPrefixes: Array<keyof typeof RequestPrefix>;
11+
gasDropOffLimit: bigint;
12+
maxGasLimit: bigint;
13+
maxMsgValue: bigint;
14+
};

0 commit comments

Comments
 (0)