Skip to content

Commit ae2f3fe

Browse files
committed
axelar transceiver
1 parent 1152d52 commit ae2f3fe

File tree

9 files changed

+1821
-150
lines changed

9 files changed

+1821
-150
lines changed

evm/ts/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"test": "jest --config ./jest.config.ts"
4545
},
4646
"dependencies": {
47+
"@axelar-network/axelarjs-sdk": "^0.17.4",
4748
"@wormhole-foundation/sdk-definitions-ntt": "1.0.0",
4849
"ethers": "^6.5.1"
4950
},

evm/ts/src/axelar.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Chain, Network } from "@wormhole-foundation/sdk-base";
2+
import {
3+
AxelarQueryAPI,
4+
Environment,
5+
EvmChain,
6+
} from "@axelar-network/axelarjs-sdk";
7+
8+
export const axelarChains: Partial<Record<Chain, EvmChain>> = {
9+
Sepolia: EvmChain.SEPOLIA,
10+
// Monad: EvmChain.MONAD,
11+
Ethereum: EvmChain.ETHEREUM,
12+
// add more as needed
13+
};
14+
15+
export async function getAxelarGasFee(
16+
network: Network,
17+
sourceChain: Chain,
18+
destinationChain: Chain,
19+
gasLimit: bigint
20+
): Promise<bigint> {
21+
const axelarQueryApi = new AxelarQueryAPI({
22+
environment:
23+
network === "Mainnet" ? Environment.MAINNET : Environment.TESTNET,
24+
});
25+
26+
const axelarSourceChain = axelarChains[sourceChain];
27+
if (!axelarSourceChain) {
28+
throw new Error(`Unsupported source chain: ${sourceChain}`);
29+
}
30+
31+
const axelarDestinationChain = axelarChains[destinationChain];
32+
if (!axelarDestinationChain) {
33+
throw new Error(`Unsupported destination chain: ${destinationChain}`);
34+
}
35+
36+
const response = await axelarQueryApi.estimateGasFee(
37+
axelarSourceChain,
38+
axelarDestinationChain,
39+
gasLimit
40+
);
41+
42+
if (typeof response !== "string") {
43+
throw new Error(`Unexpected response type: ${typeof response}`);
44+
}
45+
46+
return BigInt(response);
47+
}

evm/ts/src/multiTokenNtt.ts

Lines changed: 144 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ChainsConfig,
1212
Contracts,
1313
isNative,
14+
serialize,
1415
TokenAddress,
1516
TokenId,
1617
toNative,
@@ -30,34 +31,31 @@ import "@wormhole-foundation/sdk-evm-core";
3031
import {
3132
MultiTokenNtt,
3233
Ntt,
33-
NttTransceiver,
3434
TrimmedAmount,
35-
WormholeNttTransceiver,
3635
} from "@wormhole-foundation/sdk-definitions-ntt";
37-
import { ethers, type Provider } from "ethers";
38-
import { EvmNttWormholeTranceiver } from "./ntt.js";
39-
import { Wormhole } from "@wormhole-foundation/sdk-connect";
36+
import { Contract, ethers, Interface, type Provider } from "ethers";
4037
import {
4138
GmpManagerBindings,
4239
loadAbiVersion,
40+
MultiTokenNttBindings,
4341
MultiTokenNttManagerBindings,
4442
} from "./multiTokenNttBindings.js";
4543
import {
4644
decodeTrimmedAmount,
4745
EncodedTrimmedAmount,
4846
untrim,
4947
} from "./trimmedAmount.js";
48+
import { getAxelarGasFee } from "./axelar.js";
49+
import { encoding } from "@wormhole-foundation/sdk-base";
5050

5151
export class EvmMultiTokenNtt<N extends Network, C extends EvmChains>
5252
implements MultiTokenNtt<N, C>
5353
{
5454
readonly chainId: bigint;
55-
56-
manager: MultiTokenNttManagerBindings.NttManager;
57-
gmpManager: GmpManagerBindings.GmpManager;
58-
59-
xcvrs: EvmNttWormholeTranceiver<N, C>[];
60-
managerAddress: string;
55+
readonly abiBindings: MultiTokenNttBindings;
56+
readonly managerAddress: string;
57+
readonly manager: MultiTokenNttManagerBindings.NttManager;
58+
readonly gmpManager: GmpManagerBindings.GmpManager;
6159

6260
constructor(
6361
readonly network: N,
@@ -73,39 +71,19 @@ export class EvmMultiTokenNtt<N extends Network, C extends EvmChains>
7371
chain
7472
) as bigint;
7573

76-
this.managerAddress = contracts.multiTokenNtt.manager;
74+
this.abiBindings = loadAbiVersion(this.version);
7775

78-
const abiBindings = loadAbiVersion(this.version);
76+
this.managerAddress = contracts.multiTokenNtt.manager;
7977

80-
this.manager = abiBindings.NttManager.connect(
78+
this.manager = this.abiBindings.NttManager.connect(
8179
contracts.multiTokenNtt.manager,
8280
this.provider
8381
);
8482

85-
this.gmpManager = abiBindings.GmpManager.connect(
83+
this.gmpManager = this.abiBindings.GmpManager.connect(
8684
contracts.multiTokenNtt.gmpManager,
8785
this.provider
8886
);
89-
90-
if (contracts.multiTokenNtt.transceiver.wormhole) {
91-
this.xcvrs = [
92-
// Enable more Transceivers here
93-
new EvmNttWormholeTranceiver(
94-
// TODO: make this compatible
95-
// @ts-ignore
96-
this,
97-
contracts.multiTokenNtt.transceiver.wormhole,
98-
abiBindings!
99-
),
100-
];
101-
} else {
102-
this.xcvrs = [];
103-
}
104-
}
105-
106-
async getTransceiver(ix: number): Promise<NttTransceiver<N, C, any> | null> {
107-
// TODO: should we make an RPC call here, or just trust that the xcvrs are set up correctly?
108-
return this.xcvrs[ix] || null;
10987
}
11088

11189
async isPaused(): Promise<boolean> {
@@ -170,20 +148,6 @@ export class EvmMultiTokenNtt<N extends Network, C extends EvmChains>
170148
);
171149
}
172150

173-
encodeOptions(
174-
options: MultiTokenNtt.TransferOptions
175-
): Ntt.TransceiverInstruction[] {
176-
const ixs: Ntt.TransceiverInstruction[] = [];
177-
178-
// TODO: add comment about how if you want to use relaying, then use the executor route
179-
ixs.push({
180-
index: 0,
181-
payload: this.xcvrs[0]!.encodeFlags({ skipRelay: true }),
182-
});
183-
184-
return ixs;
185-
}
186-
187151
static async getVersion(
188152
provider: ethers.Provider,
189153
contracts: Contracts & { multiTokenNtt?: MultiTokenNtt.Contracts }
@@ -209,13 +173,110 @@ export class EvmMultiTokenNtt<N extends Network, C extends EvmChains>
209173
}
210174
}
211175

176+
async getSendTransceivers(destinationChain: Chain) {
177+
const sendTransceivers =
178+
await this.gmpManager.getSendTransceiversWithIndicesForChain(
179+
toChainId(destinationChain)
180+
);
181+
182+
return await Promise.all(
183+
sendTransceivers.map(async (transceiver) => {
184+
const type = await this.getTransceiverType(transceiver.transceiver);
185+
return {
186+
address: transceiver.transceiver,
187+
index: Number(transceiver.index),
188+
type,
189+
};
190+
})
191+
);
192+
}
193+
194+
async getReceiveTransceivers(sourceChain: Chain) {
195+
const receiveTransceivers =
196+
await this.gmpManager.getReceiveTransceiversWithIndicesForChain(
197+
toChainId(sourceChain)
198+
);
199+
200+
return await Promise.all(
201+
receiveTransceivers.map(async (transceiver) => {
202+
const type = await this.getTransceiverType(transceiver.transceiver);
203+
return {
204+
address: transceiver.transceiver,
205+
index: Number(transceiver.index),
206+
type,
207+
};
208+
})
209+
);
210+
}
211+
212+
private async getTransceiverType(
213+
transceiverAddress: string
214+
): Promise<string> {
215+
const transceiverInterface = new Interface([
216+
"function getTransceiverType() external view returns (string memory)",
217+
]);
218+
219+
const transceiverContract = new Contract(
220+
transceiverAddress,
221+
transceiverInterface,
222+
this.provider
223+
);
224+
225+
return await transceiverContract
226+
.getFunction("getTransceiverType")
227+
.staticCall();
228+
}
229+
230+
async createTransceiverInstructions(
231+
dstChain: Chain,
232+
gasLimit: bigint
233+
): Promise<Ntt.TransceiverInstruction[]> {
234+
const sendTransceivers = await this.getSendTransceivers(dstChain);
235+
236+
const instructions: Ntt.TransceiverInstruction[] = await Promise.all(
237+
sendTransceivers.map(async (transceiver) => {
238+
if (transceiver.type.toLowerCase() === "wormhole") {
239+
return {
240+
index: transceiver.index,
241+
payload: new Uint8Array([1]), // skipRelay = true
242+
};
243+
} else if (transceiver.type.toLowerCase() === "axelar") {
244+
// If we fail to fetch the axelar gas fee, then use 0 as a fallback
245+
// The user will need to manually top up the axelar gas fee later
246+
let gasFee = 0n;
247+
try {
248+
gasFee = await getAxelarGasFee(
249+
this.network,
250+
this.chain,
251+
dstChain,
252+
gasLimit
253+
);
254+
} catch {}
255+
return {
256+
index: transceiver.index,
257+
payload: encoding.bignum.toBytes(gasFee, 32),
258+
};
259+
} else {
260+
throw new Error(
261+
`Unsupported transceiver type: ${transceiver.type} at index ${transceiver.index}`
262+
);
263+
}
264+
})
265+
);
266+
267+
// the contract expects the instructions to be sorted by transceiver index
268+
instructions.sort((a, b) => a.index - b.index);
269+
270+
return instructions;
271+
}
272+
212273
async quoteDeliveryPrice(
213274
dstChain: Chain,
214-
options: MultiTokenNtt.TransferOptions
275+
instructions: Ntt.TransceiverInstruction[]
215276
): Promise<bigint> {
216277
const [, totalPrice] = await this.gmpManager.quoteDeliveryPrice(
217278
toChainId(dstChain),
218-
Ntt.encodeTransceiverInstructions(this.encodeOptions(options))
279+
Ntt.encodeTransceiverInstructions(instructions)
219280
);
220281
return totalPrice;
221282
}
@@ -228,18 +289,17 @@ export class EvmMultiTokenNtt<N extends Network, C extends EvmChains>
228289
): AsyncGenerator<EvmUnsignedTransaction<N, C>> {
229290
const senderAddress = new EvmAddress(sender).toString();
230291

231-
// TODO: get rid of this
232-
const options = {};
292+
const transceiverInstructions = await this.createTransceiverInstructions(
293+
destination.chain,
294+
800_000n // TODO: make this configurable
295+
);
233296

234297
const totalPrice = await this.quoteDeliveryPrice(
235298
destination.chain,
236-
options
299+
transceiverInstructions
237300
);
238301

239302
const receiver = universalAddress(destination);
240-
const transceiverInstructions = Ntt.encodeTransceiverInstructions(
241-
this.encodeOptions(options)
242-
);
243303

244304
let transferTx;
245305
if (isNative(token)) {
@@ -249,7 +309,9 @@ export class EvmMultiTokenNtt<N extends Network, C extends EvmChains>
249309
recipient: receiver,
250310
refundAddress: receiver,
251311
shouldQueue: false,
252-
transceiverInstructions,
312+
transceiverInstructions: Ntt.encodeTransceiverInstructions(
313+
transceiverInstructions
314+
),
253315
additionalPayload: "0x",
254316
};
255317

@@ -288,7 +350,9 @@ export class EvmMultiTokenNtt<N extends Network, C extends EvmChains>
288350
recipient: receiver,
289351
refundAddress: receiver,
290352
shouldQueue: false,
291-
transceiverInstructions,
353+
transceiverInstructions: Ntt.encodeTransceiverInstructions(
354+
transceiverInstructions
355+
),
292356
permit: {
293357
permitted: {
294358
token: token.toString(),
@@ -314,25 +378,27 @@ export class EvmMultiTokenNtt<N extends Network, C extends EvmChains>
314378
);
315379
}
316380

317-
async *redeem(attestations: MultiTokenNtt.Attestation[]) {
318-
if (attestations.length !== this.xcvrs.length)
319-
throw new Error(
320-
"Not enough attestations for the registered Transceivers"
321-
);
381+
// TODO: this only supports redeeming with a Wormhole transceiver for now
382+
async *redeem(attestation: MultiTokenNtt.Attestation) {
383+
const transceivers = await this.getReceiveTransceivers(
384+
attestation.emitterChain
385+
);
322386

323-
for (const idx in this.xcvrs) {
324-
const xcvr = this.xcvrs[idx]!;
325-
const attestation = attestations[idx];
326-
if (attestation?.payloadName !== "WormholeTransfer") {
327-
// TODO: support standard relayer attestations
328-
// which must be submitted to the delivery provider
329-
throw new Error("Invalid attestation type for redeem");
330-
}
331-
// TODO: deserialize is throwing. casting is fine for now
332-
// const serialized = serialize(attestation);
333-
// const vaa = deserialize("Ntt:WormholeTransfer", serialized);
334-
yield* xcvr.receive(attestation as unknown as WormholeNttTransceiver.VAA);
387+
const wormholeTransceiver = transceivers.find((t) => t.type === "wormhole");
388+
if (!wormholeTransceiver) {
389+
throw new Error("No Wormhole transceiver registered for this chain");
335390
}
391+
392+
const transceiver = this.abiBindings.NttTransceiver.connect(
393+
wormholeTransceiver.address,
394+
this.provider
395+
);
396+
397+
const tx = await transceiver.receiveMessage.populateTransaction(
398+
serialize(attestation)
399+
);
400+
401+
yield this.createUnsignedTx(tx, "NttTransceiver.receiveMessage");
336402
}
337403

338404
async getTokenMeta(token: TokenId): Promise<MultiTokenNtt.TokenMeta> {
@@ -485,12 +551,12 @@ export class EvmMultiTokenNtt<N extends Network, C extends EvmChains>
485551

486552
if (localToken === ethers.ZeroAddress) return null;
487553

488-
return Wormhole.tokenId(this.chain, localToken);
554+
return { chain: this.chain, address: toNative(this.chain, localToken) };
489555
}
490556

491557
async getWrappedNativeToken(): Promise<TokenId> {
492558
const wethAddress = await this.manager.WETH();
493-
return Wormhole.tokenId(this.chain, wethAddress);
559+
return { chain: this.chain, address: toNative(this.chain, wethAddress) };
494560
}
495561

496562
async calculateLocalTokenAddress(

0 commit comments

Comments
 (0)