Skip to content

Commit 0314355

Browse files
committed
sdk: Added NttExecutorRoute
1 parent 1a2a92e commit 0314355

27 files changed

Lines changed: 2694 additions & 625 deletions

evm/ts/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@wormhole-foundation/sdk-evm-ntt",
3-
"version": "0.5.0",
3+
"version": "1.0.0",
44
"repository": {
55
"type": "git",
66
"url": "git+https://github.com/wormhole-foundation/example-native-token-transfers.git"
@@ -44,14 +44,14 @@
4444
"test": "jest --config ./jest.config.ts"
4545
},
4646
"dependencies": {
47-
"@wormhole-foundation/sdk-definitions-ntt": "0.5.0",
47+
"@wormhole-foundation/sdk-definitions-ntt": "1.0.0",
4848
"ethers": "^6.5.1"
4949
},
5050
"peerDependencies": {
51-
"@wormhole-foundation/sdk-base": "^1.0.0",
52-
"@wormhole-foundation/sdk-definitions": "^1.0.0",
53-
"@wormhole-foundation/sdk-evm": "^1.0.0",
54-
"@wormhole-foundation/sdk-evm-core": "^1.0.0"
51+
"@wormhole-foundation/sdk-base": "^2.1.0",
52+
"@wormhole-foundation/sdk-definitions": "^2.1.0",
53+
"@wormhole-foundation/sdk-evm": "^2.1.0",
54+
"@wormhole-foundation/sdk-evm-core": "^2.1.0"
5555
},
5656
"devDependencies": {
5757
"@typechain/ethers-v6": "^0.5.1",
@@ -71,4 +71,4 @@
7171
}
7272
}
7373
}
74-
}
74+
}

evm/ts/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { registerProtocol } from "@wormhole-foundation/sdk-definitions";
22
import { _platform } from "@wormhole-foundation/sdk-evm";
33
import { EvmNtt } from "./ntt.js";
4+
import { EvmNttWithExecutor } from "./nttWithExecutor.js";
45
import "@wormhole-foundation/sdk-definitions-ntt";
56

67
registerProtocol(_platform, "Ntt", EvmNtt);
8+
registerProtocol(_platform, "NttWithExecutor", EvmNttWithExecutor);
79

810
export * as ethers_contracts from "./ethers-contracts/index.js";
911
export * from "./ntt.js";
12+
export * from "./nttWithExecutor.js";

evm/ts/src/ntt.ts

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,23 @@ export class EvmNttWormholeTranceiver<N extends Network, C extends EvmChains>
8080
async *setPeer<P extends Chain>(
8181
peer: ChainAddress<P>
8282
): AsyncGenerator<EvmUnsignedTransaction<N, C>> {
83-
const coreBridge = new Contract(this.manager.contracts.coreBridge!, [
84-
"function messageFee() public view returns (uint256)",
85-
],
83+
const coreBridge = new Contract(
84+
this.manager.contracts.coreBridge!,
85+
["function messageFee() public view returns (uint256)"],
8686
this.manager.provider
87-
)
88-
const messageFee = await coreBridge.getFunction("messageFee").staticCall()
87+
);
88+
const messageFee = await coreBridge.getFunction("messageFee").staticCall();
8989
const tx = await this.transceiver.setWormholePeer.populateTransaction(
9090
toChainId(peer.chain),
9191
universalAddress(peer)
9292
);
93-
yield this.manager.createUnsignedTx({
94-
...tx,
95-
value: messageFee
96-
}, "WormholeTransceiver.registerPeer");
93+
yield this.manager.createUnsignedTx(
94+
{
95+
...tx,
96+
value: messageFee,
97+
},
98+
"WormholeTransceiver.registerPeer"
99+
);
97100
}
98101

99102
async getPauser(): Promise<AccountAddress<C> | null> {
@@ -357,6 +360,7 @@ export class EvmNtt<N extends Network, C extends EvmChains>
357360

358361
async getTokenDecimals(): Promise<number> {
359362
return await EvmPlatform.getDecimals(
363+
this.network,
360364
this.chain,
361365
this.provider,
362366
this.tokenAddress
@@ -396,7 +400,9 @@ export class EvmNtt<N extends Network, C extends EvmChains>
396400

397401
ixs.push({
398402
index: 0,
399-
payload: this.xcvrs[0]!.encodeFlags({ skipRelay: !options.automatic }),
403+
payload: this.xcvrs[0]!.encodeFlags({
404+
skipRelay: !options.automatic,
405+
}),
400406
});
401407

402408
return ixs;
@@ -482,19 +488,48 @@ export class EvmNtt<N extends Network, C extends EvmChains>
482488
);
483489

484490
if (options.wrapNative) {
485-
// TODO: the contract should handle this for us
486-
const wrappedNative = new Contract(this.tokenAddress, [
487-
"function deposit() public payable",
488-
]);
489-
490-
const txReq = await wrappedNative
491-
.getFunction("deposit")
492-
.populateTransaction({ value: amount });
491+
yield this.wrapNative(sender, amount);
492+
}
493493

494-
yield this.createUnsignedTx(addFrom(txReq, senderAddress), "Ntt.Deposit");
494+
const approveTx = await this.approve(sender, amount);
495+
if (approveTx) {
496+
yield approveTx;
495497
}
496498

497-
//TODO check for ERC-2612 (permit) support on token?
499+
const receiver = universalAddress(destination);
500+
const txReq = await this.manager
501+
.getFunction("transfer(uint256,uint16,bytes32,bytes32,bool,bytes)")
502+
.populateTransaction(
503+
amount,
504+
toChainId(destination.chain),
505+
receiver,
506+
receiver,
507+
options.queue,
508+
Ntt.encodeTransceiverInstructions(this.encodeOptions(options)),
509+
{ value: totalPrice }
510+
);
511+
512+
yield this.createUnsignedTx(addFrom(txReq, senderAddress), "Ntt.transfer");
513+
}
514+
515+
async wrapNative(sender: AccountAddress<C>, amount: bigint) {
516+
const senderAddress = new EvmAddress(sender).toString();
517+
518+
// TODO: the contract should handle this for us
519+
const wrappedNative = new Contract(this.tokenAddress, [
520+
"function deposit() public payable",
521+
]);
522+
523+
const txReq = await wrappedNative
524+
.getFunction("deposit")
525+
.populateTransaction({ value: amount });
526+
527+
return this.createUnsignedTx(addFrom(txReq, senderAddress), "Ntt.Deposit");
528+
}
529+
530+
async approve(sender: AccountAddress<C>, amount: bigint) {
531+
const senderAddress = new EvmAddress(sender).toString();
532+
498533
const tokenContract = EvmPlatform.getTokenImplementation(
499534
this.provider,
500535
this.tokenAddress
@@ -510,23 +545,11 @@ export class EvmNtt<N extends Network, C extends EvmChains>
510545
amount
511546
);
512547

513-
yield this.createUnsignedTx(addFrom(txReq, senderAddress), "Ntt.Approve");
514-
}
515-
516-
const receiver = universalAddress(destination);
517-
const txReq = await this.manager
518-
.getFunction("transfer(uint256,uint16,bytes32,bytes32,bool,bytes)")
519-
.populateTransaction(
520-
amount,
521-
toChainId(destination.chain),
522-
receiver,
523-
receiver,
524-
options.queue,
525-
Ntt.encodeTransceiverInstructions(this.encodeOptions(options)),
526-
{ value: totalPrice }
548+
return this.createUnsignedTx(
549+
addFrom(txReq, senderAddress),
550+
"Ntt.Approve"
527551
);
528-
529-
yield this.createUnsignedTx(addFrom(txReq, senderAddress), "Ntt.transfer");
552+
}
530553
}
531554

532555
// TODO: should this be some map of idx to transceiver?

evm/ts/src/nttWithExecutor.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import {
2+
nativeChainIds,
3+
toChainId,
4+
type Network,
5+
} from "@wormhole-foundation/sdk-base";
6+
import {
7+
type AccountAddress,
8+
type ChainAddress,
9+
type ChainsConfig,
10+
Contracts,
11+
UnsignedTransaction,
12+
} from "@wormhole-foundation/sdk-definitions";
13+
import { Ntt, NttWithExecutor } from "@wormhole-foundation/sdk-definitions-ntt";
14+
import {
15+
EvmPlatform,
16+
type EvmPlatformType,
17+
type EvmChains,
18+
EvmAddress,
19+
} from "@wormhole-foundation/sdk-evm";
20+
import { Provider, Interface } from "ethers";
21+
import { EvmNtt } from "./ntt.js";
22+
23+
const nttManagerWithExecutorAddresses: Partial<
24+
Record<Network, Partial<Record<EvmChains, string>>>
25+
> = {
26+
Mainnet: {
27+
Arbitrum: "0x0Af42A597b0C201D4dcf450DcD0c06d55ddC1C77",
28+
Avalanche: "0x4e9Af03fbf1aa2b79A2D4babD3e22e09f18Bb8EE",
29+
Base: "0x83216747fC21b86173D800E2960c0D5395de0F30",
30+
Berachain: "0x0a2AF374Cc9CCCbB0Acc4E34B20b9d02a0f08c30",
31+
Bsc: "0x39B57Dd9908F8be02CfeE283b67eA1303Bc29fe1",
32+
Celo: "0x3d69869fcB9e1CD1F4020b637fb8256030BAc8fC",
33+
Ethereum: "0xD2D9c936165a85F27a5a7e07aFb974D022B89463",
34+
HyperEVM: "0x431017B1718b86898C7590fFcCC380DEf0456393",
35+
Linea: "0xEAa5AddB5b8939Eb73F7faF46e193EefECaF13E9",
36+
Moonbeam: "0x1365593C8bae71a55e48E105a2Bb76d5928c7DE3",
37+
Optimism: "0x85C0129bE5226C9F0Cf4e419D2fefc1c3FCa25cF",
38+
Polygon: "0x6762157b73941e36cEd0AEf54614DdE545d0F990",
39+
Scroll: "0x055625d48968f99409244E8c3e03FbE73B235a62",
40+
Sonic: "0xaCa00703bb87F31D6F9fCcc963548b48FA46DfeB",
41+
Unichain: "0x607723D6353Dae3ef62B7B277Cfabd0F4bc6CB4C",
42+
Worldchain: "0x66b1644400D51e104272337226De3EF1A820eC79",
43+
},
44+
Testnet: {
45+
Avalanche: "0x4e9Af03fbf1aa2b79A2D4babD3e22e09f18Bb8EE",
46+
BaseSepolia: "0x5845E08d890E21687F7Ebf7CbAbD360cD91c6245",
47+
Sepolia: "0x54DD7080aE169DD923fE56d0C4f814a0a17B8f41",
48+
},
49+
};
50+
51+
// Gas limits must be high enough to cover the worst-case scenario for each chain
52+
// to avoid relay failures. However, they should not be too high to reduce the
53+
// `estimatedCost` returned by the quote endpoint.
54+
const gasLimitOverrides: Partial<
55+
Record<Network, Partial<Record<EvmChains, bigint>>>
56+
> = {
57+
Mainnet: {
58+
Arbitrum: 800_000n,
59+
},
60+
Testnet: {},
61+
};
62+
63+
export class EvmNttWithExecutor<N extends Network, C extends EvmChains>
64+
implements NttWithExecutor<N, C>
65+
{
66+
readonly chainId: bigint;
67+
readonly executorAddress: string;
68+
69+
constructor(
70+
readonly network: N,
71+
readonly chain: C,
72+
readonly provider: Provider,
73+
readonly contracts: Contracts & { ntt?: Ntt.Contracts }
74+
) {
75+
this.chainId = nativeChainIds.networkChainToNativeChainId.get(
76+
network,
77+
chain
78+
) as bigint;
79+
80+
const executorAddress =
81+
nttManagerWithExecutorAddresses[this.network]?.[this.chain];
82+
if (!executorAddress)
83+
throw new Error(`Executor address not found for chain ${this.chain}`);
84+
this.executorAddress = executorAddress;
85+
}
86+
87+
static async fromRpc<N extends Network>(
88+
provider: Provider,
89+
config: ChainsConfig<N, EvmPlatformType>
90+
): Promise<EvmNttWithExecutor<N, EvmChains>> {
91+
const [network, chain] = await EvmPlatform.chainFromRpc(provider);
92+
const conf = config[chain]!;
93+
if (conf.network !== network)
94+
throw new Error(`Network mismatch: ${conf.network} != ${network}`);
95+
96+
return new EvmNttWithExecutor(
97+
network as N,
98+
chain,
99+
provider,
100+
conf.contracts
101+
);
102+
}
103+
104+
async *transfer(
105+
sender: AccountAddress<C>,
106+
destination: ChainAddress,
107+
amount: bigint,
108+
quote: NttWithExecutor.Quote,
109+
ntt: EvmNtt<N, C>,
110+
wrapNative: boolean = false
111+
): AsyncGenerator<UnsignedTransaction<N, C>> {
112+
const senderAddress = new EvmAddress(sender).toString();
113+
114+
const options = { queue: false, automatic: false };
115+
116+
// This will include any transceiver fees
117+
const deliveryPrice = await ntt.quoteDeliveryPrice(
118+
destination.chain,
119+
options
120+
);
121+
122+
if (wrapNative) {
123+
yield ntt.wrapNative(sender, amount);
124+
}
125+
126+
const tokenContract = EvmPlatform.getTokenImplementation(
127+
this.provider,
128+
ntt.tokenAddress
129+
);
130+
131+
const allowance = await tokenContract.allowance(
132+
senderAddress,
133+
this.executorAddress
134+
);
135+
136+
if (allowance < amount) {
137+
const txReq = await tokenContract.approve.populateTransaction(
138+
this.executorAddress,
139+
amount
140+
);
141+
142+
yield ntt.createUnsignedTx(txReq, "Ntt.Approve");
143+
}
144+
145+
// ABI for the INttManagerWithExecutor transfer function
146+
// TODO: type safety. typechain brings in so much boilerplate code and is soft deprecated. Use Viem instead?
147+
const abi = [
148+
"function transfer(address nttManager, uint256 amount, uint16 recipientChain, bytes32 recipientAddress, bytes32 refundAddress, bytes encodedInstructions, (uint256 value, address refundAddress, bytes signedQuote, bytes instructions) executorArgs, (uint16 dbps, address payee) feeArgs) external payable returns (uint64 msgId)",
149+
];
150+
151+
const iface = new Interface(abi);
152+
153+
const nttManager = ntt.managerAddress;
154+
const recipientChain = toChainId(destination.chain);
155+
const recipientAddress = destination.address
156+
.toUniversalAddress()
157+
.toUint8Array();
158+
const refundAddress = sender.toUniversalAddress().toUint8Array();
159+
const encodedInstructions = Ntt.encodeTransceiverInstructions(
160+
ntt.encodeOptions({ queue: false, automatic: false })
161+
);
162+
const executorArgs = {
163+
value: quote.estimatedCost,
164+
refundAddress: senderAddress,
165+
signedQuote: quote.signedQuote,
166+
instructions: quote.relayInstructions,
167+
};
168+
const feeArgs = {
169+
dbps: quote.referrerFeeDbps,
170+
payee: quote.referrer.address.toString(),
171+
};
172+
173+
const data = iface.encodeFunctionData("transfer", [
174+
nttManager,
175+
amount,
176+
recipientChain,
177+
recipientAddress,
178+
refundAddress,
179+
encodedInstructions,
180+
executorArgs,
181+
feeArgs,
182+
]);
183+
184+
const txReq = {
185+
to: this.executorAddress,
186+
data,
187+
value: quote.estimatedCost + deliveryPrice,
188+
};
189+
190+
yield ntt.createUnsignedTx(txReq, "NttWithExecutor.transfer");
191+
}
192+
193+
async estimateMsgValueAndGasLimit(
194+
recipient: ChainAddress | undefined
195+
): Promise<{ msgValue: bigint; gasLimit: bigint }> {
196+
const gasLimit = gasLimitOverrides[this.network]?.[this.chain] ?? 500_000n;
197+
return { msgValue: 0n, gasLimit };
198+
}
199+
}

0 commit comments

Comments
 (0)