diff --git a/.prettierignore b/.prettierignore index b681052..d2210e7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ -evm +/evm + diff --git a/README.md b/README.md index 8189082..7763cc0 100644 --- a/README.md +++ b/README.md @@ -8,35 +8,82 @@ spoken - this is a very complex piece of software which targets a bleeding-edge, Mistakes happen, and no matter how hard you try and whether you pay someone to audit it, it may eat your tokens, set your printer on fire or startle your cat. Cryptocurrencies are a high-risk investment, no matter how fancy. -## Testing Flow +## Testing with Tilt -> 🚧 This is a work in progress! - -First tilt up. +To run tests using Tilt, execute the following command: ```bash tilt up ``` -Next, deploy integration contracts to each chain. +This will spin up a local development environment with all necessary components including Anvil chains and the executor service. + +After Tilt is running, the test suite will run automatically in the `e2e` step. + +## Capabilities and Supported Platforms + +| Capability | Status | Description | +| ---------- | ------------ | ------------------------------------------ | +| NTT V1 | ✅ Supported | Native Token Transfer protocol integration | +| VAA V1 | ✅ Supported | Verified Action Approvals V1 protocol | + +| Platform | Status | Networks | +| -------- | ------------ | --------------------------------------- | +| EVM | ✅ Supported | Ethereum, BSC, Polygon, Avalanche, etc. | + +## Running the Docker Image + +You can run the executor using the published Docker image. To override the default configuration: ```bash -forge create ExecutorVAAv1Integration -r http://localhost:8545 --broadcast --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --constructor-args 0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78 0xD0fb39f5a3361F21457653cB70F9D0C9bD86B66B 200 -forge create ExecutorVAAv1Integration -r http://localhost:8546 --broadcast --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --constructor-args 0x79A1027a6A159502049F10906D333EC57E95F083 0x51B47D493CBA7aB97e3F8F163D6Ce07592CE4482 200 +docker run -v /path/to/your/config.json:/app/config.json \ + -e CONFIG_PATH=/app/config.json \ + example-executor:latest ``` -Finally, bun test +The configuration file should contain your chain settings, RPC endpoints, and other necessary parameters. + +## Customizing Your Anvil Environment + +The local Anvil chains can be customized to deploy additional integration contracts or modify the blockchain state for testing purposes. You can: + +- Deploy custom contracts using Forge or other deployment tools +- Modify chain state using Anvil's RPC methods +- Configure custom accounts and balances +- Set up specific testing scenarios + +Example of deploying additional contracts: ```bash -bun test +forge create YourContract -r http://localhost:8545 --private-key ``` -Click the link! It should be `pending` at first and then after a couple seconds, refresh and it should be `submitted`! +## Deployed Contracts + +The following contracts are used as part of this repository infrastructure: + +| Contract | Network | Address | Description | +| -------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| NTT Shim Contract | Sepolia | [0x54DD7080aE169DD923fE56d0C4f814a0a17B8f41](https://sepolia.etherscan.io/address/0x54DD7080aE169DD923fE56d0C4f814a0a17B8f41) | Handles NTT protocol integration | +| Executor | Sepolia | [0xD0fb39f5a3361F21457653cB70F9D0C9bD86B66B](https://sepolia.etherscan.io/address/0xD0fb39f5a3361F21457653cB70F9D0C9bD86B66B) | Main executor contract | +| MultiReceiveWithGasDropOff | Base Sepolia | [0xe3cc16Cffa085C78e5D8144C74Fa97e4Fe53d68d](https://sepolia.basescan.org/address/0xe3cc16Cffa085C78e5D8144C74Fa97e4Fe53d68d) | Manages multi-receiver operations | + +## Reference Repositories + +The following open source repositories provide additional resources and examples: -Next steps +| Repository | Description | URL | +| ------------------- | --------------------------------------------- | --------------------------------------------------------------------------- | +| Executor | Main messaging executor helper implementation | [GitHub](https://github.com/wormholelabs-xyz/example-messaging-executor) | +| Executor Helpers | Helper utilities for executor integration | [GitHub](https://github.com/wormholelabs-xyz/executor-helpers) | +| NttWithExecutor EVM | NTT integration example for EVM chains | [GitHub](https://github.com/wormholelabs-xyz/example-ntt-with-executor-evm) | + +## Using the Executor Explorer + +You can monitor and explore executor transactions using the Executor Explorer. For local development: + +``` +https://wormholelabs-xyz.github.io/executor-explorer/#/chain/10002/tx/${hash}?endpoint=http%3A%2F%2Flocalhost%3A3000&env=Testnet +``` -- Support NTT v1 -- Publish the executor docker image -- Support passing the executor chain config from env or command line so that someone using the docker image can configure the chain info -- Deploy the contracts within the e2e test and actually confirm the messages send both ways and update the contract number. -- Trigger the e2e test in CI +Replace `${hash}` with your transaction hash. The explorer will connect to your local executor instance running on port 3000. diff --git a/bun.lock b/bun.lock index ebf5607..0394de2 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@types/express": "^5.0.3", "@wormhole-foundation/sdk-base": "^2.4.0", "@wormhole-foundation/sdk-definitions": "^2.4.0", + "@wormhole-foundation/sdk-definitions-ntt": "^1.0.2", "axios": "^1.10.0", "binary-layout": "^1.3.0", "cors": "^2.8.5", @@ -70,6 +71,8 @@ "@wormhole-foundation/sdk-definitions": ["@wormhole-foundation/sdk-definitions@2.4.0", "", { "dependencies": { "@noble/curves": "^1.4.0", "@noble/hashes": "^1.3.1", "@wormhole-foundation/sdk-base": "2.4.0" } }, "sha512-Aqx3/XLaBzbt5kt70N0lnVj3acGe/DYN66R4lG7AVv7VvDTSj2PKC0qOdbKgMh+bzFbjKK03fJkpUzl/d6eo+A=="], + "@wormhole-foundation/sdk-definitions-ntt": ["@wormhole-foundation/sdk-definitions-ntt@1.0.2", "", { "peerDependencies": { "@wormhole-foundation/sdk-base": "^2.1.0", "@wormhole-foundation/sdk-definitions": "^2.1.0" } }, "sha512-HXPlWaLc/tp26WyfXCl1ndRtIJq81yunI/UqWyYcMKRabtgZUQRNJqBPYdVOV8lKVtE+4Tx3FF0OWyw7KIrNZw=="], + "abitype": ["abitype@1.0.8", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], diff --git a/chains.example.json b/chains.example.json index 7ce102f..162f2dc 100644 --- a/chains.example.json +++ b/chains.example.json @@ -11,8 +11,9 @@ "nativeDecimals": 18, "executorAddress": "0xD0fb39f5a3361F21457653cB70F9D0C9bD86B66B", "coreContractAddress": "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78", + "nttMultiReceiveWithGasDropOffAddress": "0xe3cc16Cffa085C78e5D8144C74Fa97e4Fe53d68d", "capabilities": { - "requestPrefixes": ["ERV1"], + "requestPrefixes": ["ERV1", "ERN1"], "gasDropOffLimit": "100000000000", "maxGasLimit": "1000000", "maxMsgValue": "200000000000" @@ -30,8 +31,9 @@ "nativeDecimals": 18, "coreContractAddress": "0x79A1027a6A159502049F10906D333EC57E95F083", "executorAddress": "0x51B47D493CBA7aB97e3F8F163D6Ce07592CE4482", + "nttMultiReceiveWithGasDropOffAddress": "0xe3cc16Cffa085C78e5D8144C74Fa97e4Fe53d68d", "capabilities": { - "requestPrefixes": ["ERV1"], + "requestPrefixes": ["ERV1", "ERN1"], "gasDropOffLimit": "100000000000", "maxGasLimit": "1000000", "maxMsgValue": "200000000000" diff --git a/package.json b/package.json index 8c4b0da..8b83c66 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@types/express": "^5.0.3", "@wormhole-foundation/sdk-base": "^2.4.0", "@wormhole-foundation/sdk-definitions": "^2.4.0", + "@wormhole-foundation/sdk-definitions-ntt": "^1.0.2", "axios": "^1.10.0", "binary-layout": "^1.3.0", "cors": "^2.8.5", diff --git a/src/abis/nttV1MultiReceiveWithGasDropOff.ts b/src/abis/nttV1MultiReceiveWithGasDropOff.ts new file mode 100644 index 0000000..a6688fe --- /dev/null +++ b/src/abis/nttV1MultiReceiveWithGasDropOff.ts @@ -0,0 +1,44 @@ +export const nttV1multiReceiveWithGasDropOffAbi = [ + { + type: "function", + name: "VERSION", + inputs: [], + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + { + type: "function", + name: "receiveMessages", + inputs: [ + { + name: "contracts", + type: "address[]", + internalType: "address[]", + }, + { name: "messages", type: "bytes[]", internalType: "bytes[]" }, + { + name: "payeeAddress", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "error", + name: "DropOffFailed", + inputs: [ + { name: "", type: "address", internalType: "address" }, + { name: "", type: "uint256", internalType: "uint256" }, + ], + }, + { + type: "error", + name: "InvalidParameters", + inputs: [ + { name: "", type: "uint256", internalType: "uint256" }, + { name: "", type: "uint256", internalType: "uint256" }, + ], + }, +] as const; diff --git a/src/api/quote.ts b/src/api/quote.ts index 76f20dd..e5c90a5 100644 --- a/src/api/quote.ts +++ b/src/api/quote.ts @@ -12,7 +12,7 @@ import { getTotalGasLimitAndMsgValue, signQuote, } from "../utils"; -import { EvmHandler } from "../relay/platform/evm"; +import { evmHandler } from "../relay/evm"; export const quoteHandler = async (req: Request, res: Response) => { const enabledChainIds = Object.keys(enabledChains); @@ -59,7 +59,7 @@ export const quoteHandler = async (req: Request, res: Response) => { const expiryTime = new Date(); expiryTime.setHours(expiryTime.getHours() + 1); - const dstGasPrice = await EvmHandler.getGasPrice(dstChain); + const dstGasPrice = await evmHandler.getGasPrice(dstChain); const quote: Quote = { quote: { diff --git a/src/api/status.ts b/src/api/status.ts index 0da62b3..3f7f363 100644 --- a/src/api/status.ts +++ b/src/api/status.ts @@ -1,14 +1,12 @@ import { type Request, type Response } from "express"; import { messageQueue } from "../relay/queue"; -import { - EvmHandler, - type RequestForExecutionWithId, -} from "../relay/platform/evm"; +import { evmHandler } from "../relay/evm"; import { enabledChains, type ChainConfig } from "../chains"; import { RelayStatus, type Capabilities, type RelayRequestData, + type RequestForExecutionWithId, } from "../types"; import { fromHex, @@ -88,7 +86,7 @@ export const statusHandler = async (req: Request, res: Response) => { return; } - const requestsForExecution = await EvmHandler.getRequestsForExecution( + const requestsForExecution = await evmHandler.getRequestsForExecution( txHash, chainConfig, ); diff --git a/src/chains.ts b/src/chains.ts index b58240a..0848bdc 100644 --- a/src/chains.ts +++ b/src/chains.ts @@ -12,6 +12,7 @@ export interface ChainConfig { nativeDecimals: number; capabilities: Capabilities; coreContractAddress: string; + nttMultiReceiveWithGasDropOffAddress: string; viemChain?: Chain; } diff --git a/src/consts.ts b/src/consts.ts index 59c3644..1ea495c 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -18,3 +18,6 @@ export const EMPTY_ADDRESS = padHex( "0x0000000000000000000000000000000000000000000000000000000000000000", { size: 32 }, ); + +export const NTT_TOKEN_BALANCE_STORE = + "0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00"; diff --git a/src/e2e.test.ts b/src/e2e.test.ts index 11d8ec2..3be2e66 100644 --- a/src/e2e.test.ts +++ b/src/e2e.test.ts @@ -5,11 +5,16 @@ import { sleep } from "bun"; import { expect, test } from "bun:test"; import { createPublicClient, + createTestClient, createWalletClient, + encodeAbiParameters, + formatUnits, getContract, http, isHex, + keccak256, padHex, + parseAbiParameters, toHex, type Account, type Chain, @@ -19,8 +24,9 @@ import { import { mnemonicToAccount } from "viem/accounts"; import forgeOutput from "../evm/out/ExecutorVAAv1Integration.sol/ExecutorVAAv1Integration.json"; import { enabledChains } from "./chains"; -import { ANVIL_MNEMONIC } from "./consts"; +import { ANVIL_MNEMONIC, NTT_TOKEN_BALANCE_STORE } from "./consts"; import { RelayStatus } from "./types"; +import { anvil } from "viem/chains"; const ABI = [ { @@ -265,3 +271,346 @@ test("it performs a VAA v1 relay", async () => { expect(await srcTestContract.read.number()).toBe(1n); expect(await dstTestContract.read.number()).toBe(1n); }, 60000); + +test("it performs a Ntt v1 relay", async () => { + const srcChain = enabledChains[10002]!; + const dstChain = enabledChains[10004]!; + const account = mnemonicToAccount(ANVIL_MNEMONIC, { addressIndex: 0 }); + const wNttTokenSepolia = { + manager: "0x06413c42e913327Bc9a08B7C1E362BAE7C0b9598", + token: "0x738141EFf659625F2eAD4feECDfCD94155C67f18", + transceiver: [ + { + address: "0x649fF7B32C2DE771043ea105c4aAb2D724497238", + type: "wormhole", + }, + ], + shim: "0x54DD7080aE169DD923fE56d0C4f814a0a17B8f41", + } as const; + + if (!srcChain.viemChain || !dstChain.viemChain) { + throw new Error("invalid viem chain"); + } + const srcTransport = http(srcChain.rpc); + const srcPublicClient = createPublicClient({ + chain: srcChain.viemChain, + transport: srcTransport, + }); + + const srcClient = createWalletClient({ + account, + chain: srcChain.viemChain, + transport: srcTransport, + }); + + const relayInstructions = toHex( + serializeLayout(relayInstructionsLayout, { + requests: [ + { + request: { + type: "GasInstruction", + gasLimit: 500000n, + msgValue: 0n, + }, + }, + ], + }), + ); + + const quoteResponse = await axios.post<{ + signedQuote: `0x${string}`; + estimatedCost: string; + }>("http://executor:3000/v0/quote", { + srcChain: 10002, + dstChain: 10004, + relayInstructions, + }); + + expect(BigInt(quoteResponse.data.estimatedCost)).toBeGreaterThan(1n); + + await setTokenBalance( + srcChain.rpc, + wNttTokenSepolia.token, + account.address, + 10000000000000000n, + ); + + const transferAmount = 1n * 10n ** (18n - 6n); + + const { request } = await srcPublicClient.simulateContract({ + account, + address: wNttTokenSepolia.token, + abi: Erc20TransferABI, + functionName: "approve", + args: [wNttTokenSepolia.shim, transferAmount], + }); + + const approvalTx = await srcClient.writeContract(request); + console.log(`Approval Tx: ${approvalTx}`); + + const approvalReceipt = await srcPublicClient.waitForTransactionReceipt({ + hash: approvalTx, + }); + + expect(approvalReceipt.status).toBe("success"); + + const paddedReceiverAddress = padHex(account.address, { + dir: "left", + size: 32, + }); + + const { request: transferRequest } = await srcPublicClient.simulateContract({ + account, + address: wNttTokenSepolia.shim, + abi: NttWithExecutorTransferABI, + functionName: "transfer", + args: [ + wNttTokenSepolia.manager, + transferAmount, + dstChain.wormholeChainId, + paddedReceiverAddress, + paddedReceiverAddress, + "0x01000101", + { + value: BigInt(quoteResponse.data.estimatedCost), + refundAddress: account.address, + signedQuote: quoteResponse.data.signedQuote, + instructions: relayInstructions, + }, + { + payee: account.address, + dbps: 0, + }, + ], + value: BigInt(quoteResponse.data.estimatedCost), + }); + const hash = await srcClient.writeContract(transferRequest); + + console.log( + `Request execution: https://wormholelabs-xyz.github.io/executor-explorer/#/chain/10002/tx/${hash}?endpoint=http%3A%2F%2Flocalhost%3A3000&env=Testnet`, + ); + + const transferReceipt = await srcPublicClient.waitForTransactionReceipt({ + hash, + }); + + expect(transferReceipt.status).toBe("success"); + + await srcPublicClient.waitForTransactionReceipt({ + hash, + }); + let statusResult; + while ( + !statusResult || + statusResult.data?.[0].status === RelayStatus.Pending + ) { + console.log(`Statusing tx: ${hash}`); + if (statusResult) { + await sleep(1000); + } + statusResult = await axios.post("http://executor:3000/v0/status/tx", { + chainId: srcChain.wormholeChainId, + txHash: hash, + }); + if (statusResult.data.length !== 1) { + throw new Error(`unexpected status result length`); + } + } + expect(statusResult.data?.[0].status).toBe(RelayStatus.Submitted); +}, 60000); + +const setTokenBalance = async ( + rpc: string, + tokenAddress: `0x${string}`, + accountAddress: `0x${string}` = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + balance: bigint = 10000000000000000n, +): Promise => { + const encodedBalanceValue = encodeAbiParameters( + parseAbiParameters("uint256"), + [balance], + ); + + const paddedBalanceValue = encodedBalanceValue.slice(2).padStart(64, "0"); + const storageValue = `0x${paddedBalanceValue}` as `0x${string}`; + + const balanceStorageSlot = keccak256( + encodeAbiParameters(parseAbiParameters("address, bytes32"), [ + accountAddress, + NTT_TOKEN_BALANCE_STORE, + ]), + ) as `0x${string}`; + + const anvilClient = createTestClient({ + chain: anvil, + mode: "anvil", + transport: http(rpc), + }); + + try { + const setStorageResult = await anvilClient.request({ + method: "anvil_setStorageAt", + params: [tokenAddress, balanceStorageSlot, storageValue], + }); + console.log("Storage set successfully:", setStorageResult); + + const tokenContract = getContract({ + address: tokenAddress, + abi: [ + { + name: "balanceOf", + inputs: [{ name: "account", type: "address" }], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + name: "decimals", + inputs: [], + outputs: [{ name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, + { + name: "symbol", + inputs: [], + outputs: [{ name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + ], + client: anvilClient, + }); + const decimals = await tokenContract.read.decimals(); + const newBalance = await tokenContract.read.balanceOf([accountAddress]); + + console.log( + `New balance of ${accountAddress}:`, + newBalance, + `(${formatUnits(newBalance, decimals)} human-readable)`, + `Decimals: ${decimals}`, + ); + + return newBalance; + } catch (error) { + console.error("Error setting storage:", error); + throw error; + } +}; + +const Erc20TransferABI = [ + { + inputs: [ + { + internalType: "address", + name: "guy", + type: "address", + }, + { + internalType: "uint256", + name: "wad", + type: "uint256", + }, + ], + name: "approve", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, +]; + +const NttWithExecutorTransferABI = [ + { + inputs: [ + { + internalType: "address", + name: "nttManager", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "uint16", + name: "recipientChain", + type: "uint16", + }, + { + internalType: "bytes32", + name: "recipientAddress", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "refundAddress", + type: "bytes32", + }, + { + internalType: "bytes", + name: "encodedInstructions", + type: "bytes", + }, + { + components: [ + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + { + internalType: "address", + name: "refundAddress", + type: "address", + }, + { + internalType: "bytes", + name: "signedQuote", + type: "bytes", + }, + { + internalType: "bytes", + name: "instructions", + type: "bytes", + }, + ], + internalType: "struct ExecutorArgs", + name: "executorArgs", + type: "tuple", + }, + { + components: [ + { + internalType: "uint16", + name: "dbps", + type: "uint16", + }, + { + internalType: "address", + name: "payee", + type: "address", + }, + ], + internalType: "struct FeeArgs", + name: "feeArgs", + type: "tuple", + }, + ], + name: "transfer", + outputs: [ + { + internalType: "uint64", + name: "msgId", + type: "uint64", + }, + ], + stateMutability: "payable", + type: "function", + }, +]; diff --git a/src/layouts/request.ts b/src/layouts/request.ts index f5b4fe6..5a900d0 100644 --- a/src/layouts/request.ts +++ b/src/layouts/request.ts @@ -8,22 +8,6 @@ import { fromHex, toHex } from "viem"; import { RequestPrefix } from "../types"; import { hexConversion } from "./conversions"; -export const modularMessageRequestLayout = [ - { name: "chain", binary: "uint", size: 2 }, - { name: "address", binary: "bytes", size: 32, custom: hexConversion }, - { name: "sequence", binary: "uint", size: 8 }, - { - name: "payload", - binary: "bytes", - lengthSize: 4, - custom: hexConversion, - }, -] as const satisfies Layout; - -export type ModularMessageRequest = DeriveType< - typeof modularMessageRequestLayout ->; - export const vaaV1RequestLayout = [ { name: "chain", binary: "uint", size: 2 }, { name: "address", binary: "bytes", size: 32, custom: hexConversion }, @@ -76,7 +60,6 @@ export const requestLayout = [ idSize: 4, idTag: "prefix", layouts: [ - [[0x45524d31, RequestPrefix.ERM1], modularMessageRequestLayout], [[0x45525631, RequestPrefix.ERV1], vaaV1RequestLayout], [[0x45524e31, RequestPrefix.ERN1], nttV1RequestLayout], [[0x45524331, RequestPrefix.ERC1], cctpV1RequestLayout], diff --git a/src/layouts/utils.ts b/src/layouts/utils.ts new file mode 100644 index 0000000..c7daa9e --- /dev/null +++ b/src/layouts/utils.ts @@ -0,0 +1,20 @@ +import { + relayInstructionsLayout, + type RelayInstructions, +} from "@wormhole-foundation/sdk-definitions"; +import { deserialize } from "binary-layout"; +import { fromHex, getAddress, padHex, trim, type Hex } from "viem"; + +export function deserializeRelayInstructions( + relayInstructionsBytes: `0x${string}`, +): RelayInstructions { + return deserialize( + relayInstructionsLayout, + fromHex(relayInstructionsBytes, "bytes"), + ); +} +export function trimToAddress(hex: Hex) { + return getAddress( + padHex(trim(hex, { dir: "left" }), { dir: "left", size: 20 }), + ); +} diff --git a/src/mockGuardian.ts b/src/mockGuardian.ts index abdcd94..4eed2ca 100644 --- a/src/mockGuardian.ts +++ b/src/mockGuardian.ts @@ -19,6 +19,7 @@ import { import { anvil } from "viem/chains"; import { CORE_ABI } from "./abis/core"; import { EVM_PRIVATE_KEY } from "./consts"; +import type { ChainConfig } from "./chains"; async function getWormholeMessage( rpc: string, @@ -46,13 +47,15 @@ async function getWormholeMessage( abi: CORE_ABI, logs: transaction.logs, }); + for (const topic of topics) { if ( topic.removed === false && isAddressEqual(topic.address, coreContractAddress) ) { const emitter = topic.args.sender; - return createVAA("Uint8Array", { + const paddedEmitter = padHex(emitter, { dir: "left", size: 32 }); + const vaa = createVAA("Uint8Array", { guardianSet: guardianSetIndex, timestamp: Number( ( @@ -73,6 +76,14 @@ async function getWormholeMessage( signatures: [], payload: toBytes(topic.args.payload), }); + + const vaaId = `${chainId}/${padHex(emitter, { dir: "left", size: 32 }).substring(2)}/${topic.args.sequence.toString()}`; + if ( + vaaId === + `${chainId}/${paddedEmitter.substring(2)}/${vaa.sequence.toString()}` + ) { + return vaa; + } } } } @@ -84,15 +95,17 @@ export async function mockWormhole( rpc: string, txHash: Hex, coreContractAddress: Hex, + vaaId: string, ): Promise { const vaa = await getWormholeMessage(rpc, txHash, coreContractAddress); - if (vaa) { - const guardianSet = new mocks.MockGuardians(0, [ - EVM_PRIVATE_KEY.substring(2), - ]); - const signedVaa = guardianSet.addSignatures(vaa); - const base64 = Buffer.from(serialize(signedVaa)).toString("base64"); - return base64; + + if (!vaa) { + throw new Error(`Vaa not found for txHash: ${txHash} and Vaa ID ${vaaId}`); } - throw new Error(`Vaa not found for txHash: ${txHash}.`); + + const guardianSet = new mocks.MockGuardians(0, [ + EVM_PRIVATE_KEY.substring(2), + ]); + const signedVaa = guardianSet.addSignatures(vaa); + return Buffer.from(serialize(signedVaa)).toString("base64"); } diff --git a/src/relay/evm/index.ts b/src/relay/evm/index.ts new file mode 100644 index 0000000..1bd6249 --- /dev/null +++ b/src/relay/evm/index.ts @@ -0,0 +1,131 @@ +import { + createPublicClient, + decodeEventLog, + getContract, + http, + isAddressEqual, + isHex, + padHex, + parseEventLogs, + toEventHash, + type Hex, +} from "viem"; +import { anvil } from "viem/chains"; +import { RequestForExecutionLogABI } from "../../abis/requestForExecutionLog"; +import type { ChainConfig } from "../../chains"; +import type { RequestForExecutionWithId } from "../../types"; +import type { IProtocolHandler } from "../handler"; +import { relayVAAv1 } from "./vaav1"; +import { getNttTransferMessages, relayNTTv1 } from "./nttv1"; +import type { VAA } from "@wormhole-foundation/sdk-definitions"; +import { CORE_ABI } from "../../abis/core"; + +const REQUEST_FOR_EXECUTION_TOPIC = toEventHash( + "RequestForExecution(address,uint256,uint16,bytes32,address,bytes,bytes,bytes)", +); + +export const evmHandler: IProtocolHandler = { + getGasPrice: async (chainConfig: ChainConfig): Promise => { + try { + const transport = http(chainConfig.rpc); + const client = createPublicClient({ + chain: anvil, + transport, + }); + return await client.getGasPrice(); + } catch (e) { + throw new Error(`unable to determine gas price`); + } + }, + + getRequestsForExecution: async ( + txHash: string, + chainConfig: ChainConfig, + ): Promise> => { + const results: Array = []; + + if (!isHex(txHash)) { + throw new Error(`Invalid txHash ${txHash}`); + } + + try { + const transport = http(chainConfig.rpc); + const client = createPublicClient({ + chain: anvil, + transport, + }); + + const transactionReceipt = await client.getTransactionReceipt({ + hash: txHash, + }); + + const block = await client.getBlock({ + blockNumber: transactionReceipt.blockNumber, + }); + + if (!transactionReceipt) return results; + + for ( + let logIndex = 0; + logIndex < transactionReceipt.logs.length; + logIndex++ + ) { + const log = transactionReceipt.logs[logIndex]; + + if ( + log && + log.removed === false && + isAddressEqual( + log.address, + chainConfig.executorAddress as `0x${string}`, + ) && + log.topics.length === 2 && + log.topics[0] === REQUEST_FOR_EXECUTION_TOPIC + ) { + const { + args: { + quoterAddress, + amtPaid, + dstChain, + dstAddr, + refundAddr, + signedQuote: signedQuoteBytes, + requestBytes, + relayInstructions: relayInstructionsBytes, + }, + } = decodeEventLog({ + abi: RequestForExecutionLogABI, + topics: log.topics, + data: log.data, + }); + + results.push({ + id: { + type: "Evm", + chain: chainConfig.wormholeChainId, + hash: transactionReceipt.transactionHash, + logIndex: BigInt(logIndex), + }, + amtPaid, + dstAddr, + dstChain: Number(dstChain), + quoterAddress, + refundAddr, + signedQuoteBytes, + requestBytes, + relayInstructionsBytes, + timestamp: new Date(Number(block.timestamp) * 1000), + }); + } + } + } catch (e) { + console.error(e); + } + + return results; + }, + + relayVAAv1: relayVAAv1, + relayNTTv1: relayNTTv1, + getNttTransferMessages: getNttTransferMessages, +}; diff --git a/src/relay/evm/nttv1.ts b/src/relay/evm/nttv1.ts new file mode 100644 index 0000000..6ddffe5 --- /dev/null +++ b/src/relay/evm/nttv1.ts @@ -0,0 +1,313 @@ +import "@wormhole-foundation/sdk-definitions-ntt"; +import { + createPublicClient, + createWalletClient, + fromBytes, + getContract, + http, + isAddressEqual, + isHex, + padHex, + parseEventLogs, + type Address, + type Hex, +} from "viem"; +import { anvil } from "viem/chains"; +import type { ChainConfig } from "../../chains"; +import type { RequestId } from "../../layouts/requestId"; +import { deserializePayload } from "@wormhole-foundation/sdk-definitions"; +import type { + NttTransceiver, + NttTransceiverMessageId, + NttTransceiverPayload, + RelayRequestData, + TxInfo, +} from "../../types"; +import { + deserializeRelayInstructions, + trimToAddress, +} from "../../layouts/utils"; +import { + getFirstDropOffInstruction, + getTotalGasLimitAndMsgValue, +} from "../../utils"; +import { privateKeyToAccount } from "viem/accounts"; +import { EVM_PRIVATE_KEY } from "../../consts"; +import { nttV1multiReceiveWithGasDropOffAbi } from "../../abis/nttV1MultiReceiveWithGasDropOff"; + +export const getEnabledTransceivers = async ( + chainConfig: ChainConfig, + address: `0x${string}`, +): Promise => { + const transport = http(chainConfig.rpc); + + const publicClient = createPublicClient({ + chain: anvil, + transport, + batch: { multicall: true }, + }); + + const transceiverAddresses = await getContract({ + address, + abi: [ + { + inputs: [], + name: "getTransceivers", + outputs: [ + { internalType: "address[]", name: "result", type: "address[]" }, + ], + stateMutability: "pure", + type: "function", + }, + ], + client: publicClient, + }).read.getTransceivers(); + + const getTransceiverType = async (address: `0x${string}`) => { + try { + return ( + await getContract({ + address, + abi: [ + { + type: "function", + name: "getTransceiverType", + inputs: [], + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + ], + client: publicClient, + }).read.getTransceiverType() + ) + .replaceAll("\n", "") + .replaceAll("\u0000", ""); // fix for a \n and a bunch of \u0000 + } catch (e) { + return "wormhole"; + } + }; + + const transceiverTypes = await Promise.all( + transceiverAddresses.map(getTransceiverType), + ); + return transceiverAddresses.map((address, idx) => ({ + address, + type: transceiverTypes[idx]!, + })); +}; + +export const getNttTransferMessages = async ( + chainConfig: ChainConfig, + id: RequestId, + address: `0x${string}`, + messageId: `0x${string}`, +): Promise => { + if (id.type !== "Evm") { + throw new Error(`Received a non-Evm request type ${id.type}`); + } + const transport = http(chainConfig.rpc); + const publicClient = createPublicClient({ + chain: anvil, + transport, + batch: { multicall: true }, + }); + + const transaction = await publicClient.getTransactionReceipt({ + hash: id.hash, + }); + + const transceivers = await getEnabledTransceivers(chainConfig, address); + const supportedMessages = []; + for (const transceiver of transceivers) { + if (transceiver.type === "wormhole") { + const wormhole = await getContract({ + address: transceiver.address, + abi: [ + { + type: "function", + name: "wormhole", + inputs: [], + outputs: [ + { + name: "", + type: "address", + internalType: "contract IWormhole", + }, + ], + stateMutability: "view", + }, + ], + client: publicClient, + }).read.wormhole({}); + const topics = parseEventLogs({ + eventName: "LogMessagePublished", + abi: [ + { + type: "event", + name: "LogMessagePublished", + inputs: [ + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "sequence", + type: "uint64", + indexed: false, + internalType: "uint64", + }, + { + name: "nonce", + type: "uint32", + indexed: false, + internalType: "uint32", + }, + { + name: "payload", + type: "bytes", + indexed: false, + internalType: "bytes", + }, + { + name: "consistencyLevel", + type: "uint8", + indexed: false, + internalType: "uint8", + }, + ], + anonymous: false, + }, + ], + logs: transaction.logs, + }); + + for (const topic of topics) { + if ( + topic.removed === false && + isAddressEqual(topic.address, wormhole) && + isAddressEqual( + (topic.args as { sender: Address }).sender, + transceiver.address, + ) + ) { + const payload = deserializePayload( + "Ntt:WormholeTransfer", + (topic.args as { payload: Uint8Array | string }).payload, + ); + const hexId = fromBytes(payload.nttManagerPayload.id, "hex"); + if (messageId === hexId) { + if (transceiver.type === "wormhole") { + supportedMessages.push({ + ...transceiver, + id: `${chainConfig.wormholeChainId}/${padHex(transceiver.address, { dir: "left", size: 32 }).substring(2)}/${(topic.args as { sequence: number }).sequence.toString()}`, + }); + } else { + const transceiverType: never = transceiver.type; + throw new Error(`Unsupported type: ${transceiverType}`); + } + } + } + } + } + } + return supportedMessages; +}; + +export const relayNTTv1 = async ( + chainConfig: ChainConfig, + relayRequest: RelayRequestData, + transceiversPayload: NttTransceiverPayload[], +): Promise => { + const transport = http(chainConfig.rpc); + const publicClient = createPublicClient({ + chain: chainConfig.viemChain, + transport, + }); + + const relayInstructions = deserializeRelayInstructions( + relayRequest.requestForExecution.relayInstructionsBytes, + ); + const { gasLimit } = getTotalGasLimitAndMsgValue( + relayRequest.requestForExecution.relayInstructionsBytes, + ); + const { dropOff, recipient } = getFirstDropOffInstruction( + relayInstructions, + chainConfig.capabilities.gasDropOffLimit, + ); + const transceivers = await getEnabledTransceivers( + chainConfig, + trimToAddress(relayRequest.requestForExecution.dstAddr), + ); + + const matchedTransceivers: Hex[] = []; + const matchedMessages: Hex[] = []; + + for (const transceiverMessage of transceiversPayload) { + for (const transceiver of transceivers) { + if (transceiver.type === transceiverMessage.type) { + matchedTransceivers.push(transceiver.address); + matchedMessages.push( + fromBytes(Buffer.from(transceiverMessage.payload, "base64"), "hex"), + ); + } + } + } + + try { + const account = privateKeyToAccount(EVM_PRIVATE_KEY); + + const client = createWalletClient({ + account, + chain: chainConfig.viemChain, + transport, + }); + + if (!isHex(chainConfig.nttMultiReceiveWithGasDropOffAddress)) { + throw new Error( + "The defined value for nttMultiReceiveWithGasDropOffAddress in chain config isn't a valid Hex string", + ); + } + + const { request } = await publicClient.simulateContract({ + account, + address: chainConfig.nttMultiReceiveWithGasDropOffAddress, + gas: gasLimit, + value: dropOff, + abi: nttV1multiReceiveWithGasDropOffAbi, + functionName: "receiveMessages", + args: [ + matchedTransceivers, + matchedMessages, + trimToAddress(fromBytes(recipient.address, "hex")), + ], + }); + + const hash = await client.writeContract(request); + + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + }); + + const block = await publicClient.getBlock({ blockHash: receipt.blockHash }); + const blockTime = new Date(Number(block.timestamp) * 1000); + + let totalCostValue = + receipt.effectiveGasPrice * receipt.gasUsed + (request.value || 0n); + + const txInfo = { + txHash: receipt.transactionHash, + chainId: chainConfig.wormholeChainId, + blockNumber: receipt.blockNumber, + blockTime, + cost: totalCostValue, + }; + + return [txInfo]; + } catch (e: unknown) { + console.error(e); + + throw e; + } +}; diff --git a/src/relay/evm/vaav1.ts b/src/relay/evm/vaav1.ts new file mode 100644 index 0000000..e846fd9 --- /dev/null +++ b/src/relay/evm/vaav1.ts @@ -0,0 +1,94 @@ +import { createPublicClient, createWalletClient, http, toHex } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { anvil } from "viem/chains"; +import { vaaV1ReceiveWithGasDropAbi } from "../../abis/vaaV1ReceiveWithGasDropoffAbi"; +import type { ChainConfig } from "../../chains"; +import { EVM_PRIVATE_KEY } from "../../consts"; +import type { RelayRequestData, TxInfo } from "../../types"; +import { + getFirstDropOffInstruction, + getTotalGasLimitAndMsgValue, + getTotalMsgValueFromGasInstructions, +} from "../../utils"; +import { + deserializeRelayInstructions, + trimToAddress, +} from "../../layouts/utils"; + +export const relayVAAv1 = async ( + chainConfig: ChainConfig, + relayRequest: RelayRequestData, + base64Vaa: string, +): Promise> => { + const transport = http(chainConfig.rpc); + const publicClient = createPublicClient({ + chain: anvil, + transport, + }); + + const { maxMsgValue, gasDropOffLimit } = chainConfig.capabilities; + + const relayInstructions = deserializeRelayInstructions( + relayRequest.requestForExecution.relayInstructionsBytes, + ); + + const { gasLimit } = getTotalGasLimitAndMsgValue( + relayRequest.requestForExecution.relayInstructionsBytes, + ); + + const relayMsgValue = getTotalMsgValueFromGasInstructions( + relayInstructions, + maxMsgValue, + ); + + const { dropOff, recipient } = getFirstDropOffInstruction( + relayInstructions, + gasDropOffLimit, + ); + + const account = privateKeyToAccount(EVM_PRIVATE_KEY); + + const client = createWalletClient({ + account, + chain: chainConfig.viemChain, + transport, + }); + + const payloadHex = toHex(Buffer.from(base64Vaa, "base64")); + + const { request } = await publicClient.simulateContract({ + account, + address: "0x13b62003C8b126Ec0748376e7ab22F79Fb8bbDF2", + gas: gasLimit, + value: relayMsgValue + dropOff, + abi: vaaV1ReceiveWithGasDropAbi, + functionName: "receiveMessage", + args: [ + trimToAddress(relayRequest.requestForExecution.dstAddr), + payloadHex, + trimToAddress(toHex(recipient.address)), + dropOff, + ], + }); + + const hash = await client.writeContract(request); + + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + }); + + const block = await publicClient.getBlock({ blockHash: receipt.blockHash }); + const blockTime = new Date(Number(block.timestamp) * 1000); + + let totalCostValue = + receipt.effectiveGasPrice * receipt.gasUsed + (request.value || 0n); + + const txInfo = { + txHash: receipt.transactionHash, + chainId: chainConfig.wormholeChainId, + blockNumber: receipt.blockNumber, + blockTime, + cost: totalCostValue, + }; + return [txInfo]; +}; diff --git a/src/relay/handler.ts b/src/relay/handler.ts new file mode 100644 index 0000000..ffb813e --- /dev/null +++ b/src/relay/handler.ts @@ -0,0 +1,39 @@ +import type { Hex } from "viem"; +import type { ChainConfig } from "../chains"; +import type { RequestId } from "../layouts/requestId"; +import type { + NttTransceiverMessageId, + NttTransceiverPayload, + RelayRequestData, + RequestForExecutionWithId, + TxInfo, +} from "../types"; +import type { VAA } from "@wormhole-foundation/sdk-definitions"; + +export interface IProtocolHandler { + getGasPrice(chainConfig: ChainConfig): Promise; + + getRequestsForExecution( + txHash: string, + chainConfig: ChainConfig, + ): Promise>; + + relayVAAv1( + chainConfig: ChainConfig, + relayRequest: RelayRequestData, + base64Vaa: string, + ): Promise>; + + relayNTTv1( + chainConfig: ChainConfig, + relayRequest: RelayRequestData, + transceiversPayload: NttTransceiverPayload[], + ): Promise; + + getNttTransferMessages( + chainConfig: ChainConfig, + id: RequestId, + address: `0x${string}`, + messageId: `0x${string}`, + ): Promise; +} diff --git a/src/relay/platform/evm.ts b/src/relay/platform/evm.ts deleted file mode 100644 index 1c90454..0000000 --- a/src/relay/platform/evm.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { - relayInstructionsLayout, - type RelayInstructions, -} from "@wormhole-foundation/sdk-definitions"; -import { deserialize } from "binary-layout"; -import { - createPublicClient, - createWalletClient, - decodeEventLog, - fromHex, - getAddress, - http, - isAddressEqual, - isHex, - padHex, - toEventHash, - toHex, - trim, - type Hex, -} from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { anvil } from "viem/chains"; -import { RequestForExecutionLogABI } from "../../abis/requestForExecutionLog"; -import { vaaV1ReceiveWithGasDropAbi } from "../../abis/vaaV1ReceiveWithGasDropoffAbi"; -import type { ChainConfig } from "../../chains"; -import { EVM_PRIVATE_KEY } from "../../consts"; -import type { RequestId } from "../../layouts/requestId"; -import type { - RelayRequestData, - RequestForExecution, - TxInfo, -} from "../../types"; -import { - getFirstDropOffInstruction, - getTotalGasLimitAndMsgValue, - getTotalMsgValueFromGasInstructions, -} from "../../utils"; - -const REQUEST_FOR_EXECUTION_TOPIC = toEventHash( - "RequestForExecution(address,uint256,uint16,bytes32,address,bytes,bytes,bytes)", -); - -export type RequestForExecutionWithId = RequestForExecution & { - id: RequestId; -}; - -export class EvmHandler { - private constructor() {} - - static async getGasPrice(chainConfig: ChainConfig): Promise { - try { - const transport = http(chainConfig.rpc); - const client = createPublicClient({ - chain: anvil, - transport, - }); - return await client.getGasPrice(); - } catch (e) { - throw new Error(`unable to determine gas price`); - } - } - - static async getRequestsForExecution( - txHash: string, - chainConfig: ChainConfig, - ): Promise> { - const results: Array = []; - - if (!isHex(txHash)) { - throw new Error(`Invalid txHash ${txHash}`); - } - - try { - const transport = http(chainConfig.rpc); - const client = createPublicClient({ - chain: anvil, - transport, - }); - - const transactionReceipt = await client.getTransactionReceipt({ - hash: txHash, - }); - - const block = await client.getBlock({ - blockNumber: transactionReceipt.blockNumber, - }); - - if (!transactionReceipt) return results; - - for ( - let logIndex = 0; - logIndex < transactionReceipt.logs.length; - logIndex++ - ) { - const log = transactionReceipt.logs[logIndex]; - - if ( - log && - log.removed === false && - isAddressEqual( - log.address, - chainConfig.executorAddress as `0x${string}`, - ) && - log.topics.length === 2 && - log.topics[0] === REQUEST_FOR_EXECUTION_TOPIC - ) { - const { - args: { - quoterAddress, - amtPaid, - dstChain, - dstAddr, - refundAddr, - signedQuote: signedQuoteBytes, - requestBytes, - relayInstructions: relayInstructionsBytes, - }, - } = decodeEventLog({ - abi: RequestForExecutionLogABI, - topics: log.topics, - data: log.data, - }); - - results.push({ - id: { - type: "Evm", - chain: chainConfig.wormholeChainId, - hash: transactionReceipt.transactionHash, - logIndex: BigInt(logIndex), - }, - amtPaid, - dstAddr, - dstChain: Number(dstChain), - quoterAddress, - refundAddr, - signedQuoteBytes, - requestBytes, - relayInstructionsBytes, - timestamp: new Date(Number(block.timestamp) * 1000), - }); - } - } - } catch (e) { - console.error(e); - } - - return results; - } - - static async relayVAAv1( - chainConfig: ChainConfig, - relayRequest: RelayRequestData, - base64Vaa: string, - ): Promise> { - const transport = http(chainConfig.rpc); - const publicClient = createPublicClient({ - chain: anvil, - transport, - }); - - const { maxMsgValue, gasDropOffLimit } = chainConfig.capabilities; - - const relayInstructions = deserializeRelayInstructions( - relayRequest.requestForExecution.relayInstructionsBytes, - ); - - const { gasLimit } = getTotalGasLimitAndMsgValue( - relayRequest.requestForExecution.relayInstructionsBytes, - ); - - const relayMsgValue = getTotalMsgValueFromGasInstructions( - relayInstructions, - maxMsgValue, - ); - - const { dropOff, recipient } = getFirstDropOffInstruction( - relayInstructions, - gasDropOffLimit, - ); - - const account = privateKeyToAccount(EVM_PRIVATE_KEY); - - const client = createWalletClient({ - account, - chain: chainConfig.viemChain, - transport, - }); - - const payloadHex = toHex(Buffer.from(base64Vaa, "base64")); - - const { request } = await publicClient.simulateContract({ - account, - address: "0x13b62003C8b126Ec0748376e7ab22F79Fb8bbDF2", - gas: gasLimit, - value: relayMsgValue + dropOff, - abi: vaaV1ReceiveWithGasDropAbi, - functionName: "receiveMessage", - args: [ - trimToAddress(relayRequest.requestForExecution.dstAddr), - payloadHex, - trimToAddress(toHex(recipient.address)), - dropOff, - ], - }); - - const hash = await client.writeContract(request); - - const receipt = await publicClient.waitForTransactionReceipt({ - hash, - }); - - const block = await publicClient.getBlock({ blockHash: receipt.blockHash }); - const blockTime = new Date(Number(block.timestamp) * 1000); - - let totalCostValue = - receipt.effectiveGasPrice * receipt.gasUsed + (request.value || 0n); - - const txInfo = { - txHash: receipt.transactionHash, - chainId: chainConfig.wormholeChainId, - blockNumber: receipt.blockNumber, - blockTime, - cost: totalCostValue, - }; - return [txInfo]; - } -} - -function deserializeRelayInstructions( - relayInstructionsBytes: `0x${string}`, -): RelayInstructions { - return deserialize( - relayInstructionsLayout, - fromHex(relayInstructionsBytes, "bytes"), - ); -} -function trimToAddress(hex: Hex) { - return getAddress( - padHex(trim(hex, { dir: "left" }), { dir: "left", size: 20 }), - ); -} diff --git a/src/relay/relayer.ts b/src/relay/relayer.ts index e33602e..6525093 100644 --- a/src/relay/relayer.ts +++ b/src/relay/relayer.ts @@ -1,14 +1,18 @@ -import { isHex } from "viem"; +import { fromHex, isHex } from "viem"; import { enabledChains } from "../chains"; import { mockWormhole } from "../mockGuardian"; import { RelayAbortedError, RequestPrefix, UnsupportedRelayRequestError, + type NttTransceiverPayload, type RelayRequestData, type TxInfo, } from "../types"; -import { EvmHandler } from "./platform/evm"; +import { evmHandler } from "./evm"; +import { requestIdLayout } from "../layouts/requestId"; +import { deserialize } from "binary-layout"; +import { trimToAddress } from "../layouts/utils"; export const processRelayRequests = async ( relayRequest: RelayRequestData, @@ -48,31 +52,70 @@ export const processRelayRequests = async ( ); } + if (!isHex(relayRequest.txHash)) { + throw new Error(`TxHash not hex!`); + } + + if (!isHex(srcChainConfig.coreContractAddress)) { + throw new Error(`Core contract not hex!`); + } + let relayedTransactions: Array = []; switch (prefix) { case RequestPrefix.ERV1: - if (!isHex(relayRequest.txHash)) { - throw new Error(`TxHash not hex!`); - } - if (!isHex(srcChainConfig.coreContractAddress)) { - throw new Error(`Core contract not hex!`); - } - const base64Vaa = await mockWormhole( + const vaaId = `${request.chain}/${request.address.substring(2)}/${request.sequence.toString()}`; + + const payload = await mockWormhole( srcChainConfig.rpc, relayRequest.txHash, srcChainConfig.coreContractAddress, + vaaId, ); - relayedTransactions = await EvmHandler.relayVAAv1( + if (!payload) { + throw new Error("No Vaa found for the transaction."); + } + + relayedTransactions = await evmHandler.relayVAAv1( dstChainConfig, relayRequest, - base64Vaa, + payload, + ); + break; + case RequestPrefix.ERN1: + const messages = await evmHandler.getNttTransferMessages( + srcChainConfig, + deserialize(requestIdLayout, fromHex(relayRequest.id, "bytes")), + trimToAddress(request.srcManager), + request.messageId, + ); + + const messagesWithPayloads: NttTransceiverPayload[] = []; + for (const message of messages) { + if (message.type === "wormhole") { + const payload = await mockWormhole( + srcChainConfig.rpc, + relayRequest.txHash, + srcChainConfig.coreContractAddress, + message.id, + ); + if (!payload) { + throw new Error( + `Expected Vaa for id ${message.id} has not been found`, + ); + } + messagesWithPayloads.push({ ...message, payload }); + } + } + + relayedTransactions = await evmHandler.relayNTTv1( + dstChainConfig, + relayRequest, + messagesWithPayloads, ); break; case RequestPrefix.ERC2: case RequestPrefix.ERC1: - case RequestPrefix.ERM1: - case RequestPrefix.ERN1: default: throw new UnsupportedRelayRequestError( `Request of type ${prefix} not supported.`, diff --git a/src/types.ts b/src/types.ts index 16b1f9f..22ded33 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,8 @@ import type { SignedQuote } from "@wormhole-foundation/sdk-definitions"; import type { RequestLayout } from "./layouts/request"; +import type { RequestId } from "./layouts/requestId"; export enum RequestPrefix { - ERM1 = "ERM1", // MM ERV1 = "ERV1", // VAA_V1 ERN1 = "ERN1", // NTT_V1 ERC1 = "ERC1", // CCTP_V1 @@ -16,6 +16,10 @@ export type Capabilities = { maxMsgValue: bigint; }; +export type RequestForExecutionWithId = RequestForExecution & { + id: RequestId; +}; + export type RequestForExecution = { quoterAddress: `0x${string}`; amtPaid: bigint; @@ -72,3 +76,18 @@ export class UnsupportedRelayRequestError extends Error { this.name = "UnsupportedRelayRequestError"; } } + +// NttV1 + +export interface NttTransceiver { + address: `0x${string}`; + type: string; +} + +export interface NttTransceiverMessageId extends NttTransceiver { + id: string; +} + +export interface NttTransceiverPayload extends NttTransceiverMessageId { + payload: string; +} diff --git a/src/utils.ts b/src/utils.ts index 2d3af46..90f36f9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,7 +6,7 @@ import { type RelayInstructions, } from "@wormhole-foundation/sdk-definitions"; import { deserialize, serialize } from "binary-layout"; -import { concat, fromBytes, fromHex, keccak256, toHex } from "viem"; +import { concat, fromBytes, fromHex, keccak256 } from "viem"; import { sign } from "viem/accounts"; import { ScaledMath } from "./lib/ScaledMath"; import { EMPTY_ADDRESS } from "./consts";