|
| 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