Skip to content

Commit 7812b52

Browse files
committed
sdk: multi-token-ntt protocol support
Adds support for the MultiTokenNtt protocol along with two new routes: MultiTokenNtt{Manual,Executor}Route.
1 parent e95b15d commit 7812b52

30 files changed

+9912
-59
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { MultiTokenNtt } from "@wormhole-foundation/sdk-definitions-ntt";
2+
import { EvmMultiTokenNtt } from "../src/multiTokenNtt.js";
3+
import { toUniversal } from "@wormhole-foundation/sdk-definitions";
4+
5+
describe("calculateLocalTokenAddress", () => {
6+
it("should calculate the correct local token address", async () => {
7+
const tokenImplementation = "0xbd7312fA1d9433ab2616FBE7aC615A58D81D2c8E";
8+
const creationCode =
9+
"0x604060808152610416908138038061001681610218565b93843982019181818403126102135780516001600160a01b038116808203610213576020838101516001600160401b0394919391858211610213570186601f820112156102135780519061007161006c83610253565b610218565b918083528583019886828401011161021357888661008f930161026e565b813b156101b9577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b031916841790556000927fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8480a28051158015906101b2575b61010b575b855160d190816103458239f35b855194606086019081118682101761019e578697849283926101889952602788527f416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c87890152660819985a5b195960ca1b8a8901525190845af4913d15610194573d9061017a61006c83610253565b91825281943d92013e610291565b508038808080806100fe565b5060609250610291565b634e487b7160e01b84526041600452602484fd5b50826100f9565b855162461bcd60e51b815260048101859052602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201526c1bdd08184818dbdb9d1c9858dd609a1b6064820152608490fd5b600080fd5b6040519190601f01601f191682016001600160401b0381118382101761023d57604052565b634e487b7160e01b600052604160045260246000fd5b6001600160401b03811161023d57601f01601f191660200190565b60005b8381106102815750506000910152565b8181015183820152602001610271565b919290156102f357508151156102a5575090565b3b156102ae5790565b60405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606490fd5b8251909150156103065750805190602001fd5b6044604051809262461bcd60e51b825260206004830152610336815180928160248601526020868601910161026e565b601f01601f19168101030190fdfe608060405236156054577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc54600090819081906001600160a01b0316368280378136915af43d82803e156050573d90f35b3d90fd5b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc54600090819081906001600160a01b0316368280378136915af43d82803e156050573d90f3fea26469706673582212206554f4df1904914343564cd467a416ab7dfbd4626b3eb40104613d037cd433f264736f6c63430008130033";
10+
const expectedAddress = "0xCaf00B2fB2fa0EAE51ecAfef7e45f13fF1BB448a";
11+
12+
const originalToken: MultiTokenNtt.OriginalTokenId = {
13+
chain: "Sepolia",
14+
// @ts-ignore
15+
address: toUniversal(
16+
"Sepolia",
17+
"0x738141EFf659625F2eAD4feECDfCD94155C67f18"
18+
),
19+
};
20+
21+
const tokenMeta = {
22+
name: "W Token",
23+
symbol: "Ws",
24+
decimals: 18,
25+
};
26+
27+
const mockMultiTokenNtt = {
28+
tokenImplementation: jest.fn().mockResolvedValue(tokenImplementation),
29+
tokenProxyCreationCode: jest.fn().mockResolvedValue(creationCode),
30+
};
31+
32+
// Minimal implementation of MultiTokenNtt for testing
33+
const multiTokenNtt = {
34+
multiTokenNtt: mockMultiTokenNtt,
35+
managerAddress: "0x600D3C45Cd002E7359D12597Bb8058a0C32A20Df",
36+
chain: "Monad",
37+
calculateLocalTokenAddress:
38+
EvmMultiTokenNtt.prototype.calculateLocalTokenAddress,
39+
};
40+
41+
const result = await multiTokenNtt.calculateLocalTokenAddress(
42+
originalToken,
43+
tokenMeta
44+
);
45+
46+
expect(result.toString()).toBe(expectedAddress);
47+
});
48+
});

evm/ts/src/axelar.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { Chain, Network } from "@wormhole-foundation/sdk-base";
2+
3+
// The point if this module is to use direct API calls instead of importing the entire @axelar-network/axelarjs-sdk to stay lightweight.
4+
5+
// Axelar chains: https://github.com/axelarnetwork/axelarjs-sdk/blob/53a957deb1209325b1e3d109e0985a64db6d9901/src/constants/EvmChain.ts#L1
6+
export const axelarChains: Partial<Record<Chain, string>> = {
7+
Ethereum: "ethereum",
8+
Monad: "monad",
9+
Sepolia: "ethereum-sepolia",
10+
// add more as needed
11+
};
12+
13+
// https://github.com/axelarnetwork/axelarjs-sdk/blob/53a957deb1209325b1e3d109e0985a64db6d9901/src/libs/TransactionRecoveryApi/AxelarRecoveryApi.ts#L16
14+
export enum GMPStatus {
15+
SRC_GATEWAY_CALLED = "source_gateway_called",
16+
DEST_GATEWAY_APPROVED = "destination_gateway_approved",
17+
DEST_EXECUTED = "destination_executed",
18+
EXPRESS_EXECUTED = "express_executed",
19+
DEST_EXECUTE_ERROR = "error",
20+
DEST_EXECUTING = "executing",
21+
APPROVING = "approving",
22+
FORECALLED = "forecalled",
23+
FORECALLED_WITHOUT_GAS_PAID = "forecalled_without_gas_paid",
24+
NOT_EXECUTED = "not_executed",
25+
NOT_EXECUTED_WITHOUT_GAS_PAID = "not_executed_without_gas_paid",
26+
INSUFFICIENT_FEE = "insufficient_fee",
27+
UNKNOWN_ERROR = "unknown_error",
28+
CANNOT_FETCH_STATUS = "cannot_fetch_status",
29+
SRC_GATEWAY_CONFIRMED = "confirmed",
30+
}
31+
32+
export interface GMPError {
33+
txHash: string;
34+
chain: string;
35+
message: string;
36+
}
37+
38+
export function getAxelarApiUrl(network: Network): string {
39+
return network === "Mainnet"
40+
? "https://api.axelarscan.io"
41+
: "https://testnet.api.axelarscan.io";
42+
}
43+
44+
export function getAxelarChain(chain: Chain): string {
45+
const axelarChain = axelarChains[chain];
46+
if (!axelarChain) {
47+
throw new Error(`Unsupported axelar chain: ${chain}`);
48+
}
49+
return axelarChain;
50+
}
51+
52+
export async function getAxelarGasFee(
53+
network: Network,
54+
sourceChain: Chain,
55+
destinationChain: Chain,
56+
gasLimit: bigint,
57+
timeoutMs = 10000
58+
): Promise<bigint> {
59+
const url = `${getAxelarApiUrl(network)}/gmp/estimateGasFee`;
60+
const axelarSourceChain = getAxelarChain(sourceChain);
61+
const axelarDestinationChain = getAxelarChain(destinationChain);
62+
63+
const controller = new AbortController();
64+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
65+
66+
// Set a minimum fee of 1 to avoid 0-fee issue with relays not proceeding
67+
// past the gas paid step
68+
let fee = 1n;
69+
70+
try {
71+
const response = await fetch(url, {
72+
method: "POST",
73+
headers: {
74+
"Content-Type": "application/json",
75+
},
76+
body: JSON.stringify({
77+
sourceChain: axelarSourceChain,
78+
destinationChain: axelarDestinationChain,
79+
gasMultiplier: "auto",
80+
gasLimit: gasLimit.toString(),
81+
}),
82+
signal: controller.signal,
83+
});
84+
85+
if (!response.ok) {
86+
const errorText = await response.text();
87+
throw new Error(
88+
`Failed to estimate gas fee: ${response.status} ${errorText}`
89+
);
90+
}
91+
92+
const result = await response.json();
93+
94+
const parsedFee = BigInt(result);
95+
if (parsedFee > 0n) {
96+
fee = parsedFee;
97+
}
98+
} finally {
99+
clearTimeout(timeoutId);
100+
}
101+
102+
return fee;
103+
}
104+
105+
export async function getAxelarTransactionStatus(
106+
network: Network,
107+
sourceChain: Chain,
108+
txHash: string,
109+
timeoutMs = 10000
110+
): Promise<{ status: GMPStatus | string; error?: GMPError }> {
111+
const url = `${getAxelarApiUrl(network)}/gmp/searchGMP`;
112+
113+
const axelarSourceChain = getAxelarChain(sourceChain);
114+
115+
const controller = new AbortController();
116+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
117+
118+
try {
119+
const response = await fetch(url, {
120+
method: "POST",
121+
headers: {
122+
"Content-Type": "application/json",
123+
},
124+
body: JSON.stringify({
125+
sourceChain: axelarSourceChain,
126+
txHash: txHash,
127+
}),
128+
signal: controller.signal,
129+
});
130+
131+
if (!response.ok) {
132+
const errorText = await response.text();
133+
throw new Error(
134+
`Failed to get transaction status: ${response.status} ${errorText}`
135+
);
136+
}
137+
138+
const result = await response.json();
139+
if (!result.data || result.data.length === 0) {
140+
throw new Error("No transaction details found");
141+
}
142+
143+
const txDetails = result.data[0];
144+
return {
145+
status: parseGMPStatus(txDetails),
146+
error: parseGMPError(txDetails),
147+
};
148+
} finally {
149+
clearTimeout(timeoutId);
150+
}
151+
}
152+
153+
export function parseGMPStatus(response: any): GMPStatus | string {
154+
const { error, status } = response;
155+
156+
if (status === "error" && error) return GMPStatus.DEST_EXECUTE_ERROR;
157+
else if (status === "executed") return GMPStatus.DEST_EXECUTED;
158+
else if (status === "approved") return GMPStatus.DEST_GATEWAY_APPROVED;
159+
else if (status === "called") return GMPStatus.SRC_GATEWAY_CALLED;
160+
else if (status === "executing") return GMPStatus.DEST_EXECUTING;
161+
else {
162+
return status;
163+
}
164+
}
165+
166+
export function parseGMPError(response: any): GMPError | undefined {
167+
if (response.error) {
168+
return {
169+
message: response.error.error.message,
170+
txHash: response.error.sourceTransactionHash,
171+
chain: response.error.chain,
172+
};
173+
} else if (response.is_insufficient_fee) {
174+
return {
175+
message: "Insufficient gas",
176+
txHash: response.call.transaction.hash,
177+
chain: response.call.chain,
178+
};
179+
}
180+
}
181+
182+
export function getAxelarExplorerUrl(network: Network, txHash: string): string {
183+
return network === "Mainnet"
184+
? `https://axelarscan.io/gmp/${txHash}`
185+
: `https://testnet.axelarscan.io/gmp/${txHash}`;
186+
}

0 commit comments

Comments
 (0)