Skip to content

Commit c1e2815

Browse files
committed
capabilities & quote endpoints
1 parent 6fc2f26 commit c1e2815

File tree

12 files changed

+373
-11
lines changed

12 files changed

+373
-11
lines changed

bun.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@types/express": "^5.0.3",
88
"@wormhole-foundation/sdk-base": "^2.4.0",
99
"@wormhole-foundation/sdk-definitions": "^2.4.0",
10+
"binary-layout": "^1.3.0",
1011
"express": "^5.1.0",
1112
"viem": "^2.31.7",
1213
},
@@ -47,7 +48,7 @@
4748

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

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

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@types/express": "^5.0.3",
1717
"@wormhole-foundation/sdk-base": "^2.4.0",
1818
"@wormhole-foundation/sdk-definitions": "^2.4.0",
19+
"binary-layout": "^1.3.0",
1920
"express": "^5.1.0",
2021
"viem": "^2.31.7"
2122
}

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

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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
gasPriceDecimals: number;
9+
nativeDecimals: number;
10+
capabilities: Capabilities;
11+
}
12+
13+
export const enabledChains: Record<number, ChainConfig> = {
14+
10002: {
15+
wormholeChainId: 10002,
16+
evmChainId: 11155111,
17+
rpc: "http://anvil-eth-sepolia:8545",
18+
name: "Ethereum Sepolia",
19+
gasPriceDecimals: 18,
20+
nativeDecimals: 18,
21+
capabilities: {
22+
requestPrefixes: [RequestPrefix.ERV1],
23+
gasDropOffLimit: 100_000_000_000n,
24+
maxGasLimit: 1_000_000n,
25+
maxMsgValue: 100_000_000_000n * 2n,
26+
},
27+
},
28+
10004: {
29+
wormholeChainId: 10004,
30+
evmChainId: 84532,
31+
rpc: "http://anvil-base-sepolia:8545",
32+
name: "Base Sepolia",
33+
gasPriceDecimals: 18,
34+
nativeDecimals: 18,
35+
capabilities: {
36+
requestPrefixes: [RequestPrefix.ERV1],
37+
gasDropOffLimit: 100_000_000_000n,
38+
maxGasLimit: 1_000_000n,
39+
maxMsgValue: 100_000_000_000n * 2n,
40+
},
41+
},
42+
};

src/consts.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@ const account = mnemonicToAccount(
66
{ addressIndex: 9 }
77
);
88

9+
export const PAYEE_PUBLIC_KEY = account.address;
10+
911
export const EVM_PUBLIC_KEY = account.address;
1012
export const EVM_PRIVATE_KEY = toHex(account.getHdKey().privateKey || "0x");
13+
14+
export const QUOTER_PUBLIC_KEY = account.address;
15+
export const QUOTER_PRIVATE_KEY = toHex(account.getHdKey().privateKey || "0x");

src/index.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
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";
4+
5+
// @ts-ignore
6+
BigInt.prototype.toJSON = function () {
7+
// Can also be JSON.rawJSON(this.toString());
8+
return this.toString();
9+
};
310

411
await overrideGuardianSet(
512
"http://anvil-eth-sepolia:8545",
@@ -11,16 +18,12 @@ await overrideGuardianSet(
1118
);
1219

1320
const app = express();
21+
1422
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-
});
23+
app.post("/v0/quote", quoteHandler);
24+
app.post("/v0/status/tx", statusHandler);
25+
app.get("/v0/capabilities", capabilitiesHandler);
26+
2427
const server = app.listen(3000, () => {
2528
console.log(`Server is running at http://localhost:3000`);
2629
});

src/lib/ScaledMath.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export const ScaledMath = {
2+
min(value: bigint, ...values: Array<bigint>) {
3+
for (const v of values) {
4+
if (v < value) {
5+
value = v;
6+
}
7+
}
8+
9+
return value;
10+
},
11+
max(value: bigint, ...values: Array<bigint>) {
12+
for (const v of values) {
13+
if (v > value) {
14+
value = v;
15+
}
16+
}
17+
18+
return value;
19+
},
20+
21+
normalize(amount: bigint, from: number, to: number) {
22+
if (from > to) {
23+
return amount / 10n ** BigInt(from - to);
24+
} else if (from < to) {
25+
return amount * 10n ** BigInt(to - from);
26+
}
27+
return amount;
28+
},
29+
30+
mul(a: bigint, b: bigint, decimals: number) {
31+
return (a * b) / 10n ** BigInt(decimals);
32+
},
33+
div(a: bigint, b: bigint, decimals: number) {
34+
return (a * 10n ** BigInt(decimals)) / b;
35+
},
36+
} as const;

0 commit comments

Comments
 (0)