From 9afd65cf6a652637bab29a54afa692501b97f4ab Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 13 Aug 2025 13:36:24 +0530 Subject: [PATCH 01/10] chore: add from field --- relayer-cli/src/utils/proof.ts | 8 +++++++- relayer-cli/src/utils/relay.ts | 15 ++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/relayer-cli/src/utils/proof.ts b/relayer-cli/src/utils/proof.ts index 524bd7b0..771329f1 100644 --- a/relayer-cli/src/utils/proof.ts +++ b/relayer-cli/src/utils/proof.ts @@ -5,6 +5,9 @@ interface MessageSentData { to: { id: string; }; + msgSender: { + id: string; + }; data: string; } @@ -34,12 +37,15 @@ const getMessageDataToRelay = async ( to { id } + msgSender { + id + } data } }` )) as MessageSentsDataResponse; - return [result[`messageSents`][0].to.id, result[`messageSents`][0].data]; + return [result[`messageSents`][0].to.id, result[`messageSents`][0].msgSender.id, result[`messageSents`][0].data]; } catch (e) { console.log(e); return undefined; diff --git a/relayer-cli/src/utils/relay.ts b/relayer-cli/src/utils/relay.ts index 767bcd0b..fb7f82af 100644 --- a/relayer-cli/src/utils/relay.ts +++ b/relayer-cli/src/utils/relay.ts @@ -59,8 +59,8 @@ const relay = async (chainId: number, nonce: number, network: Network) => { getMessageDataToRelay(chainId, veaInboxAddress, nonce), ]); if (!messageData) throw new DataError("relay message data"); - const [to, data] = messageData; - const txn = await veaOutbox.sendMessage(proof, nonce, to, data); + const [to, from, data] = messageData; + const txn = await veaOutbox.sendMessage(proof, nonce, to, from, data); const receipt = await txn.wait(); return receipt; }; @@ -127,10 +127,10 @@ const relayBatch = async ({ fetchProofAtCount(chainId, nonce, count, veaInboxAddress), fetchMessageDataToRelay(chainId, veaInboxAddress, nonce), ]); - const [to, data] = messageData; + const [to, from, data] = messageData; try { - await veaOutbox.sendMessage.staticCall(proof, nonce, to, data); - const callData = veaOutbox.interface.encodeFunctionData("sendMessage", [proof, nonce, to, data]); + await veaOutbox.sendMessage.staticCall(proof, nonce, to, from, data); + const callData = veaOutbox.interface.encodeFunctionData("sendMessage", [proof, nonce, to, from, data]); datas.push(callData); targets.push(veaOutboxAddress); values.push(0); @@ -142,6 +142,7 @@ const relayBatch = async ({ } } if (batchMessages > 0) { + console.log(targets, datas); const gasLimit = await batcher.batchSend.estimateGas(targets, values, datas); const tx = await batcher.batchSend(targets, values, datas, { gasLimit }); const receipt = await tx.wait(); @@ -193,9 +194,9 @@ const relayAllFrom = async ( getProofAtCount(chainId, x, count, veaInboxAddress), getMessageDataToRelay(chainId, veaInboxAddress, x), ]); - const [to, data] = messageData; + const [to, from, data] = messageData; - const callData = veaOutbox.interface.encodeFunctionData("sendMessage", [proof, x, to, data]); + const callData = veaOutbox.interface.encodeFunctionData("sendMessage", [proof, x, to, from, data]); datas.push(callData); targets.push(veaContracts[network].veaOutbox.address); values.push(0); From 9bf7c9a2cf1c92d989902f5e4585bfce0c395a0e Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 13 Aug 2025 13:36:48 +0530 Subject: [PATCH 02/10] chore: update tests for added field --- relayer-cli/src/utils/relay.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relayer-cli/src/utils/relay.test.ts b/relayer-cli/src/utils/relay.test.ts index ad9ff754..4633c366 100644 --- a/relayer-cli/src/utils/relay.test.ts +++ b/relayer-cli/src/utils/relay.test.ts @@ -61,7 +61,7 @@ describe("relay", () => { fetchVeaOutbox = jest.fn().mockReturnValue(veaOutboxMock); fetchProofAtCount = jest.fn().mockResolvedValue([]); - fetchMessageDataToRelay = jest.fn().mockResolvedValue(["to", "data"]); + fetchMessageDataToRelay = jest.fn().mockResolvedValue(["to", "from", "data"]); mockWait = jest.fn().mockResolvedValue("receipt"); mockBatchSend = jest.fn().mockResolvedValue({ wait: mockWait }); From a16ccf9fc42ebbb8500f3c72bfda8e3bc5065121 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 13 Aug 2025 13:38:02 +0530 Subject: [PATCH 03/10] fix: node hash decoding for msg sender --- relayer-subgraph-inbox/src/vea-inbox.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/relayer-subgraph-inbox/src/vea-inbox.ts b/relayer-subgraph-inbox/src/vea-inbox.ts index 189a902f..b3a536aa 100644 --- a/relayer-subgraph-inbox/src/vea-inbox.ts +++ b/relayer-subgraph-inbox/src/vea-inbox.ts @@ -30,12 +30,12 @@ export function handleMessageSent(event: MessageSentEvent): void { let _to = new ByteArray(20); for (let i = 0; i < 20; i++) _to[i] = msgData[i + 8]; - let dataLength = msgData.length - 28; - let _data = new ByteArray(dataLength); - for (let i = 0; i < dataLength; i++) _data[i] = msgData[i + 28]; - let _msgSender = new ByteArray(20); - for (let i = 0; i < 20; i++) _msgSender[i] = _data[i + 16]; + for (let i = 0; i < 20; i++) _msgSender[i] = msgData[i + 28]; + + let dataLength = msgData.length - 48; + let _data = new ByteArray(dataLength); + for (let i = 0; i < dataLength; i++) _data[i] = msgData[i + 48]; entity.inbox = event.address; entity.nonce = BigInt.fromByteArray(_nonce.reverse() as ByteArray); From 966dd6ac33ec0f33b35d9f79980c35438ac1bba8 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 15 Oct 2025 16:10:08 +0530 Subject: [PATCH 04/10] feat: hashi executor --- relayer-cli/.env.dist | 5 +- relayer-cli/src/consts/bridgeRoutes.ts | 9 ++ relayer-cli/src/relayer.ts | 13 ++- relayer-cli/src/utils/graphQueries.ts | 21 ++++ relayer-cli/src/utils/hashi.ts | 128 ++++++++++++++++++++++++ relayer-cli/src/utils/hashiHelpers.ts | 15 +++ relayer-cli/src/utils/relayerHelpers.ts | 14 ++- relayer-cli/state/devnet_11155111.json | 5 + 8 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 relayer-cli/src/utils/graphQueries.ts create mode 100644 relayer-cli/src/utils/hashi.ts create mode 100644 relayer-cli/src/utils/hashiHelpers.ts create mode 100644 relayer-cli/state/devnet_11155111.json diff --git a/relayer-cli/.env.dist b/relayer-cli/.env.dist index f068b6c5..be3a3a55 100644 --- a/relayer-cli/.env.dist +++ b/relayer-cli/.env.dist @@ -21,4 +21,7 @@ TRANSACTION_BATCHER_CONTRACT_SEPOLIA=0xe7953da7751063d0a41ba727c32c762d3523ade8 TRANSACTION_BATCHER_CONTRACT_CHIADO=0xcC0a08D4BCC5f91ee9a1587608f7a2975EA75d73 # Ensure the path ends with a trailing slash ("/") -STATE_DIR="/home/user/vea/relayer-cli/state/" \ No newline at end of file +STATE_DIR="/home/user/vea/relayer-cli/state/" + +# Hashi Executor enabler +HASHI_EXECUTOR_ENABLED=true \ No newline at end of file diff --git a/relayer-cli/src/consts/bridgeRoutes.ts b/relayer-cli/src/consts/bridgeRoutes.ts index 91ff3d2f..ed87caec 100644 --- a/relayer-cli/src/consts/bridgeRoutes.ts +++ b/relayer-cli/src/consts/bridgeRoutes.ts @@ -17,7 +17,11 @@ interface IBridge { epochPeriod: number; veaContracts: { [key in Network]: VeaContracts }; batcherAddress: string; + rpcInbox: string; rpcOutbox: string; + yahoAddress?: string; // Hashi (Yaru) contract address + yaruAddress?: string; // Hashi (Yaru) contract address + hashiAddress?: string; // Hashi (Yaru) contract address } type VeaContracts = { @@ -60,7 +64,11 @@ const bridges: { [chainId: number]: IBridge } = { epochPeriod: 7200, veaContracts: arbToEthContracts, batcherAddress: process.env.TRANSACTION_BATCHER_CONTRACT_SEPOLIA!, + rpcInbox: process.env.RPC_ARBITRUM_SEPOLIA!, rpcOutbox: process.env.RPC_SEPOLIA!, + yahoAddress: "0xDbdF80c87f414fac8342e04D870764197bD3bAC7", // Hashi (Yaho) contract address on Arbitrum Sepolia + yaruAddress: "0x231e48AAEaAC6398978a1dBA4Cd38fcA208Ec391", // Hashi (Yaru) contract address on Sepolia + hashiAddress: "0x78E4ae687De18B3B71Ccd0e8a3A76Fed49a02A02", // Hashi (Yaru) contract address on Sepolia }, 10200: { chainId: 10200, @@ -68,6 +76,7 @@ const bridges: { [chainId: number]: IBridge } = { epochPeriod: 3600, veaContracts: arbToGnosisContracts, batcherAddress: process.env.TRANSACTION_BATCHER_CONTRACT_CHIADO!, + rpcInbox: process.env.RPC_ARBITRUM_SEPOLIA!, rpcOutbox: process.env.RPC_CHIADO!, }, }; diff --git a/relayer-cli/src/relayer.ts b/relayer-cli/src/relayer.ts index 6c8746ea..397b2300 100644 --- a/relayer-cli/src/relayer.ts +++ b/relayer-cli/src/relayer.ts @@ -3,7 +3,7 @@ import { EventEmitter } from "node:events"; import { ethers } from "ethers"; import { relayBatch, relayAllFrom } from "./utils/relay"; import { - initialize as initializeNonce, + initialize as initializeNonces, updateStateFile, delay, setupExitHandlers, @@ -14,6 +14,7 @@ import { import { initialize as initializeEmitter } from "./utils/logger"; import { BotEvents } from "./utils/botEvents"; import { getEpochPeriod, Network } from "./consts/bridgeRoutes"; +import { runHashiExecutor } from "./utils/hashi"; interface RelayerConfig { networkConfigs: RelayerNetworkConfig[]; @@ -59,9 +60,15 @@ async function processNetworkConfig( await setupExitHandlers(chainId, shutdownManager, network, emitter); - let nonce = await initializeNonce(chainId, network, emitter); + let { nonce, hashiNonce } = await initializeNonces(chainId, network, emitter); if (nonce == null) return currentDelay; + const hashiExecutorEnabled = process.env.HASHI_EXECUTOR_ENABLED === "true"; + if (hashiExecutorEnabled) { + // Execute messages on Hashi (Yaru contract) + hashiNonce = await runHashiExecutor(chainId, network, hashiNonce); + } + const toRelayAll = senders[0] === ethers.ZeroAddress; nonce = toRelayAll ? await relayBatch({ chainId, network, nonce, maxBatchSize, emitter }) @@ -69,7 +76,7 @@ async function processNetworkConfig( if (nonce == null) return currentDelay; - await updateStateFile(chainId, Math.floor(Date.now() / 1000), nonce, network, emitter); + await updateStateFile(chainId, Math.floor(Date.now() / 1000), nonce, hashiNonce, network, emitter); if (network === Network.DEVNET) { return 1000 * 10; // 10 seconds for devnet diff --git a/relayer-cli/src/utils/graphQueries.ts b/relayer-cli/src/utils/graphQueries.ts new file mode 100644 index 00000000..62b4c901 --- /dev/null +++ b/relayer-cli/src/utils/graphQueries.ts @@ -0,0 +1,21 @@ +import request from "graphql-request"; + +async function getVeaMsgTrnx(nonce: number, inboxAddress: string) { + console.log(`Fetching transaction hashes for nonce ${nonce} from inbox ${inboxAddress}`); + try { + const subgraph = process.env.RELAYER_SUBGRAPH; + const query = `{messageSents(first: 1, where: {nonce: ${nonce}, inbox: "${inboxAddress}"}) { + id + transactionHash + }}`; + const result = (await request(`https://api.studio.thegraph.com/query/${subgraph}`, query)) as { + messageSents: { id: string; transactionHash: string }[]; + }; + return result.messageSents.map((trnx) => trnx.transactionHash); + } catch (e) { + console.log(e); + return []; + } +} + +export { getVeaMsgTrnx }; diff --git a/relayer-cli/src/utils/hashi.ts b/relayer-cli/src/utils/hashi.ts new file mode 100644 index 00000000..9003f2d3 --- /dev/null +++ b/relayer-cli/src/utils/hashi.ts @@ -0,0 +1,128 @@ +import { JsonRpcProvider, Interface, getAddress, zeroPadBytes, Contract, Wallet } from "ethers"; +import { getVeaMsgTrnx } from "./graphQueries"; +import { getVeaInbox } from "./ethers"; +import { getBridgeConfig } from "../consts/bridgeRoutes"; +import { executeMessagesAbi, messageDispatchedAbi, thresholdViewAbi } from "./hashiHelpers"; + +const SOURCE_CHAIN_ID = 421613; // Arbitrum Sepolia + +interface HashiMessage { + hashiContextNonce: number; + sender: string; + destinationChainId: number; + receiver: string; + threshold: number; + data: string; + reporters: string[]; + adapters: string[]; +} + +type VeaNonceToHashiMessage = { + nonce: number; + hashiMessage: HashiMessage; +}; + +async function runHashiExecutor(chainId: number, network: string, nonce: number) { + const bridgeConfig = getBridgeConfig(chainId); + const { veaContracts, rpcInbox } = bridgeConfig; + const veaInboxAddress = veaContracts[network].veaInbox.address; + const privateKey = process.env.PRIVATE_KEY; + const veaInbox = getVeaInbox(veaInboxAddress, privateKey, rpcInbox, chainId); + const inboxCount = await veaInbox.count(); + const executableNonces: VeaNonceToHashiMessage[] = []; + while (nonce < inboxCount) { + // ToDo: Add cooldown periods for nonces that cannot be executed. + const toExecute = await toExecuteMesssage(chainId, nonce, veaInboxAddress, rpcInbox); + if (toExecute) { + executableNonces.push(toExecute); + } + nonce++; + } + console.log(`Found ${executableNonces.length} executable nonces on Hashi`); + nonce = await executeBatchOnHashi(chainId, executableNonces); + console.log(`Processed up to nonce ${nonce} on Hashi`); + return nonce; +} + +async function executeBatchOnHashi(chainId: number, params: VeaNonceToHashiMessage[]): Promise { + const { yaruAddress, rpcOutbox } = getBridgeConfig(chainId); + const maxPerTx = 20; + const provider = new JsonRpcProvider(rpcOutbox); + const signer = new Wallet(process.env.PRIVATE_KEY!, provider); + const yaru = new Contract(yaruAddress, executeMessagesAbi, signer); + const iface = new Interface(executeMessagesAbi); + + let cursor = 0; + let lastNonceProcessed = 0; + + while (cursor < params.length) { + const chunk = params.slice(cursor, cursor + maxPerTx); + + const tx = await yaru.executeMessages(chunk); + const receipt = await tx.wait(); + + cursor += chunk.length; + lastNonceProcessed = Number(params[Math.min(cursor, params.length) - 1].nonce); + + console.log(`Executed ${chunk.length} messages in tx ${receipt.transactionHash}`); + } + + return ++lastNonceProcessed; +} + +async function toExecuteMesssage( + chainId: number, + nonce: number, + inboxAddress: string, + rpcInbox: string +): Promise { + const hashes = await getVeaMsgTrnx(nonce, inboxAddress); + const provider = new JsonRpcProvider(rpcInbox); + const bridgeConfig = getBridgeConfig(chainId); + const yahoAddress = bridgeConfig.yahoAddress?.toLowerCase(); + const receipt = await provider.getTransactionReceipt(hashes[0]); + let executeNonce: VeaNonceToHashiMessage | null = null; + let hashiMessage: HashiMessage | null = null; + + for (const log of receipt.logs) { + if (log.address.toLowerCase() === yahoAddress) { + const logIFace = new Interface(messageDispatchedAbi); + const logData = log.data; + const decodedLog = logIFace.decodeEventLog("MessageDispatched", logData, log.topics); + const message = decodedLog.message; + hashiMessage = { + hashiContextNonce: message[0], + destinationChainId: message[1], + threshold: message[2], + sender: message[3], + receiver: message[4], + data: message[5], + reporters: message[6], + adapters: message[7], + }; + const thresholdMet = await isThresholdMet(hashiMessage); + if (thresholdMet) { + executeNonce = { nonce, hashiMessage }; + } + break; + } + } + return executeNonce; +} + +async function isThresholdMet(message: HashiMessage): Promise { + const bridgeConfig = getBridgeConfig(message.destinationChainId); + const hashiAddress = bridgeConfig.hashiAddress; + const provider = new JsonRpcProvider(bridgeConfig.rpcOutbox); + const domain = BigInt(SOURCE_CHAIN_ID); // uint256 + const id = BigInt(message.hashiContextNonce); // or messageId if contract expects that + const threshold = BigInt(message.threshold); // uint256 + const adapters = message.adapters.map((address) => getAddress(address)); // address[] + const iface = new Interface(thresholdViewAbi); + const data = iface.encodeFunctionData("checkHashWithThresholdFromAdapters", [domain, id, threshold, adapters]); + const ret = await provider.call({ to: hashiAddress, data }); + const [ok] = iface.decodeFunctionResult("checkHashWithThresholdFromAdapters", ret); + return ok; +} + +export { executeBatchOnHashi, runHashiExecutor, toExecuteMesssage }; diff --git a/relayer-cli/src/utils/hashiHelpers.ts b/relayer-cli/src/utils/hashiHelpers.ts new file mode 100644 index 00000000..4371686a --- /dev/null +++ b/relayer-cli/src/utils/hashiHelpers.ts @@ -0,0 +1,15 @@ +export const dispatchAbi = [ + "function dispatchMessageToAdapters(uint256 targetChainId,uint256 threshold,address receiver,bytes data,address[] reporters,address[] adapters)", +]; + +export const messageDispatchedAbi = [ + "event MessageDispatched(uint256 indexed messageId, (uint256 nonce,uint256 targetChainId,uint256 threshold,address sender,address receiver,bytes data,address[] reporters,address[] adapters) message)", +]; + +export const thresholdViewAbi = [ + "function checkHashWithThresholdFromAdapters(uint256 domain,uint256 id,uint256 threshold,address[] adapters) view returns (bool)", +]; + +export const executeMessagesAbi = [ + "function executeMessages((uint256,uint256,uint256,address,address,bytes,address[],address[])[] messages) external returns (bytes[])", +]; diff --git a/relayer-cli/src/utils/relayerHelpers.ts b/relayer-cli/src/utils/relayerHelpers.ts index e89e50c8..38c67991 100644 --- a/relayer-cli/src/utils/relayerHelpers.ts +++ b/relayer-cli/src/utils/relayerHelpers.ts @@ -25,7 +25,7 @@ async function initialize( setLock: typeof claimLock = claimLock, syncStateFile: typeof updateStateFile = updateStateFile, fileSystem: typeof fs = fs -): Promise { +): Promise<{ nonce: number | null; hashiNonce: number | null }> { setLock(network, chainId); emitter.emit(BotEvents.LOCK_CLAIMED); // STATE_DIR is absolute path of the directory where the state files are stored @@ -35,19 +35,23 @@ async function initialize( if (!fileSystem.existsSync(stateFile)) { // No state file so initialize starting now const tsnow = Math.floor(Date.now() / 1000); - await syncStateFile(chainId, tsnow, 0, network, emitter); + await syncStateFile(chainId, tsnow, 0, 0, network, emitter); } // print pwd for debugging emitter.emit(BotEvents.LOCK_DIRECTORY, process.cwd()); const chain_state_raw = fileSystem.readFileSync(stateFile, { encoding: "utf8" }); const chain_state = JSON.parse(chain_state_raw); - let nonce = 0; + let nonce = 0, + hashiNonce = 0; if ("nonce" in chain_state) { nonce = chain_state["nonce"]; } + if ("hashiNonce" in chain_state) { + hashiNonce = chain_state["hashiNonce"]; + } - return nonce; + return { nonce, hashiNonce }; } /** @@ -63,6 +67,7 @@ async function updateStateFile( chainId: number, createdTimestamp: number, nonceFrom: number, + hashiNonceFrom: number, network: string, emitter: EventEmitter, fileSystem: typeof fs = fs, @@ -77,6 +82,7 @@ async function updateStateFile( const json = { ts: createdTimestamp, nonce: nonceFrom, + hashiNonce: hashiNonceFrom, }; fileSystem.writeFileSync(chain_state_file, JSON.stringify(json), { encoding: "utf8" }); } diff --git a/relayer-cli/state/devnet_11155111.json b/relayer-cli/state/devnet_11155111.json new file mode 100644 index 00000000..c198764c --- /dev/null +++ b/relayer-cli/state/devnet_11155111.json @@ -0,0 +1,5 @@ +{ + "ts": 1760357592, + "nonce": 5, + "hashiNonce": 1 +} From f0ce06fe31a93db9c4c10196bb7368770e18170f Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 22 Oct 2025 18:31:35 +0530 Subject: [PATCH 05/10] feat: hashi executor --- relayer-cli/src/relayer.ts | 4 ++-- relayer-cli/src/utils/relay.ts | 1 - relayer-cli/src/utils/relayerHelpers.test.ts | 16 +++++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/relayer-cli/src/relayer.ts b/relayer-cli/src/relayer.ts index 397b2300..e3b36361 100644 --- a/relayer-cli/src/relayer.ts +++ b/relayer-cli/src/relayer.ts @@ -65,8 +65,8 @@ async function processNetworkConfig( const hashiExecutorEnabled = process.env.HASHI_EXECUTOR_ENABLED === "true"; if (hashiExecutorEnabled) { - // Execute messages on Hashi (Yaru contract) - hashiNonce = await runHashiExecutor(chainId, network, hashiNonce); + // Execute messages on Hashi + hashiNonce = await runHashiExecutor({ chainId, network, nonce: hashiNonce, emitter }); } const toRelayAll = senders[0] === ethers.ZeroAddress; diff --git a/relayer-cli/src/utils/relay.ts b/relayer-cli/src/utils/relay.ts index fb7f82af..92123a49 100644 --- a/relayer-cli/src/utils/relay.ts +++ b/relayer-cli/src/utils/relay.ts @@ -142,7 +142,6 @@ const relayBatch = async ({ } } if (batchMessages > 0) { - console.log(targets, datas); const gasLimit = await batcher.batchSend.estimateGas(targets, values, datas); const tx = await batcher.batchSend(targets, values, datas, { gasLimit }); const receipt = await tx.wait(); diff --git a/relayer-cli/src/utils/relayerHelpers.test.ts b/relayer-cli/src/utils/relayerHelpers.test.ts index 6900959e..abb57323 100644 --- a/relayer-cli/src/utils/relayerHelpers.test.ts +++ b/relayer-cli/src/utils/relayerHelpers.test.ts @@ -22,8 +22,8 @@ describe("relayerHelpers", () => { describe("initialize", () => { it("should claimLock and create a state file if it doesn't exist", async () => { fileSystem.existsSync.mockReturnValue(false); - fileSystem.readFileSync.mockReturnValue('{"nonce":0}'); - const nonce = await initialize( + fileSystem.readFileSync.mockReturnValue('{"hashiNonce":0,"nonce":0}'); + const { hashiNonce, nonce } = await initialize( chainId, network, emitter as any, @@ -32,13 +32,14 @@ describe("relayerHelpers", () => { fileSystem as any ); expect(claimLock).toHaveBeenCalledWith(network, chainId); - expect(mockUpdateStateFile).toHaveBeenCalledWith(chainId, expect.any(Number), 0, network, emitter); + expect(mockUpdateStateFile).toHaveBeenCalledWith(chainId, expect.any(Number), 0, 0, network, emitter); expect(nonce).toBe(0); + expect(hashiNonce).toBe(0); }); it("should claimLock and return nonce from existing state file", async () => { fileSystem.existsSync.mockReturnValue(true); - fileSystem.readFileSync.mockReturnValue('{"nonce":10}'); - const nonce = await initialize( + fileSystem.readFileSync.mockReturnValue('{"hashiNonce":10,"nonce":10}'); + const { hashiNonce, nonce } = await initialize( chainId, network, emitter as any, @@ -49,6 +50,7 @@ describe("relayerHelpers", () => { expect(claimLock).toHaveBeenCalledWith(network, chainId); expect(mockUpdateStateFile).not.toHaveBeenCalled(); expect(nonce).toBe(10); + expect(hashiNonce).toBe(10); }); }); @@ -56,10 +58,10 @@ describe("relayerHelpers", () => { it("should write a state file with the provided nonce", async () => { const createdTimestamp = 123456; const fileDirectory = process.env.STATE_DIR + network + "_" + chainId + ".json"; - await updateStateFile(chainId, createdTimestamp, 10, network, emitter as any, fileSystem as any, releaseLock); + await updateStateFile(chainId, createdTimestamp, 10, 10, network, emitter as any, fileSystem as any, releaseLock); expect(fileSystem.writeFileSync).toHaveBeenCalledWith( fileDirectory, - JSON.stringify({ ts: createdTimestamp, nonce: 10 }), + JSON.stringify({ ts: createdTimestamp, nonce: 10, hashiNonce: 10 }), { encoding: "utf8" } ); expect(releaseLock).toHaveBeenCalledWith(network, chainId); From 9c8c6b4996432565147e3a1af0a49775f9c43985 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 22 Oct 2025 18:32:11 +0530 Subject: [PATCH 06/10] feat: hashi helpers --- relayer-cli/src/utils/botEvents.ts | 6 + relayer-cli/src/utils/hashi.test.ts | 231 ++++++++++++ relayer-cli/src/utils/hashi.ts | 213 ++++++++--- relayer-cli/src/utils/hashiHelpers.ts | 15 - relayer-cli/src/utils/hashiHelpers/abi.ts | 342 ++++++++++++++++++ .../src/utils/hashiHelpers/hashiMsgUtils.ts | 64 ++++ .../src/utils/hashiHelpers/hashiTypes.ts | 22 ++ relayer-cli/src/utils/logger.ts | 11 + 8 files changed, 837 insertions(+), 67 deletions(-) create mode 100644 relayer-cli/src/utils/hashi.test.ts delete mode 100644 relayer-cli/src/utils/hashiHelpers.ts create mode 100644 relayer-cli/src/utils/hashiHelpers/abi.ts create mode 100644 relayer-cli/src/utils/hashiHelpers/hashiMsgUtils.ts create mode 100644 relayer-cli/src/utils/hashiHelpers/hashiTypes.ts diff --git a/relayer-cli/src/utils/botEvents.ts b/relayer-cli/src/utils/botEvents.ts index 969eba95..778301a9 100644 --- a/relayer-cli/src/utils/botEvents.ts +++ b/relayer-cli/src/utils/botEvents.ts @@ -17,4 +17,10 @@ export enum BotEvents { RELAY_BATCH = "relay_batch", RELAY_ALL_FROM = "relay_all_from", MESSAGE_EXECUTION_FAILED = "message_execution_failed", + + // Hashi executor + EXECUTING_HASHI = "executing_hashi", + HASHI_EXECUTED = "hashi_executed", + HASHI_BATCH_TXN = "hashi_batch_txn", + HASHI_EXECUTION_FAILED = "hashi_execution_failed", } diff --git a/relayer-cli/src/utils/hashi.test.ts b/relayer-cli/src/utils/hashi.test.ts new file mode 100644 index 00000000..b00f0ffc --- /dev/null +++ b/relayer-cli/src/utils/hashi.test.ts @@ -0,0 +1,231 @@ +import { EventEmitter } from "node:events"; +import { BotEvents } from "./botEvents"; +import { toExecuteMesssage, runHashiExecutor } from "./hashi"; +import { VeaNonceToHashiMessage, HashiExecutionStatus, HashiMessage } from "./hashiHelpers/hashiTypes"; + +class MockEmitter extends EventEmitter { + emit(event: string | symbol, ...args: any[]): boolean { + // Prevent console logs for BotEvents during tests + if (Object.values(BotEvents).includes(event as BotEvents)) { + return true; + } + return super.emit(event, ...args); + } +} + +describe("hashi", () => { + let mockEmitter = new MockEmitter(); + const veaOutboxAddress = "0x123"; + const currentCount = 3; + const network = "testing" as any; + const chainId = 1; + const nonce = 0; + + let fetchBridgeConfig: jest.Mock; + let fetchCount: jest.Mock; + let fetchVeaInbox: jest.Mock; + let fetchBatcher: jest.Mock; + let fetchVeaMsgTrnx: jest.Mock; + let provider: jest.Mock; + let logIFace: jest.Mock; + + let mockWait: jest.Mock; + let mockBatchSend: jest.Mock & { estimateGas?: jest.Mock }; + + let veaOutboxMock: any; + + beforeEach(() => { + fetchBridgeConfig = jest.fn().mockReturnValue({ + batcherAddress: veaOutboxAddress, + yahoAddress: "0xYaho", + veaContracts: { + [network]: { + veaInbox: { address: "0xInbox", abi: ["dummyInboxAbi"] }, + veaOutbox: { address: veaOutboxAddress, abi: ["dummyOutboxAbi"] }, + }, + }, + rpcOutbox: "https://rpc.example.com", + }); + + fetchCount = jest.fn().mockResolvedValue(currentCount); + + veaOutboxMock = { + isMsgRelayed: jest.fn().mockResolvedValue(false), + interface: { + encodeFunctionData: jest.fn().mockImplementation((fnName, args) => { + return `callData_${args[1]}`; + }), + }, + sendMessage: { + staticCall: jest.fn().mockResolvedValue(true), + }, + }; + + fetchVeaInbox = jest.fn().mockReturnValue({ + count: fetchCount, + }); + fetchVeaMsgTrnx = jest.fn().mockResolvedValue(["0x123"]); + + mockWait = jest.fn().mockResolvedValue("receipt"); + mockBatchSend = jest.fn().mockResolvedValue({ wait: mockWait }); + + mockBatchSend.estimateGas = jest.fn().mockResolvedValue(600000); + + fetchBatcher = jest.fn().mockReturnValue({ + batchSend: mockBatchSend, + }); + + provider = { + getTransactionReceipt: jest.fn().mockResolvedValue({ logs: [{ address: "0xYaho" }] }), + } as any; + + logIFace = { + decodeEventLog: jest.fn().mockReturnValue({ + message: [ + BigInt(1), //hashi nonce + BigInt(2), // targetChainId + BigInt(3), // threshold + "0xSender", // sender + "0xReceiver", // receiver + "0xData", // data + ["0xReporter1"], // reporters + ["0xAdapter1", "0xAdapter2"], // adapters + ], + }), + } as any; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + describe("toExecuteMessage", () => { + it("should verify if the vea message is executed using Hashi and is executable", async () => { + const result = await toExecuteMesssage({ + chainId, + nonce, + veaInboxAddress: "0xInbox", + rpcInbox: "https://rpc.inbox.example.com", + fetchVeaMsgTrnx, + fetchBridgeConfig, + hasThresholdMet: jest.fn().mockResolvedValue(HashiExecutionStatus.EXECUTABLE), + provider: provider as any, + logIFace: logIFace as any, + }); + expect(result).not.toBeNull(); + expect(result?.nonce).toBe(nonce); + expect(result?.hashiMessage.nonce).toBe(BigInt(1)); + }); + it("should return null if the vea message is not executable via Hashi", async () => { + const result = await toExecuteMesssage({ + chainId, + nonce, + veaInboxAddress: "0xInbox", + rpcInbox: "https://rpc.inbox.example.com", + fetchVeaMsgTrnx, + fetchBridgeConfig, + hasThresholdMet: jest.fn().mockResolvedValue(false), + provider: provider as any, + logIFace: logIFace as any, + }); + expect(result).toBeNull(); + }); + it("should return null if there is no Hashi message in the logs", async () => { + const providerNoLogs = { + getTransactionReceipt: jest.fn().mockResolvedValue({ logs: [] }), + } as any; + + const result = await toExecuteMesssage({ + chainId, + nonce, + veaInboxAddress: "0xInbox", + rpcInbox: "https://rpc.inbox.example.com", + fetchVeaMsgTrnx, + fetchBridgeConfig, + hasThresholdMet: jest.fn(), + provider: providerNoLogs as any, + logIFace: logIFace as any, + }); + expect(result).toBeNull(); + }); + }); + + describe("runHashiExecutor", () => { + let mockToExecuteMesssage: jest.Mock; + let mockExecuteMessage: jest.Mock; + const mockHashiMessage1: HashiMessage = { + nonce: 3, + targetChainId: 2, + threshold: 1, + sender: "0xSender", + receiver: "0xReceiver", + data: "0xData", + reporters: ["0xReporter1"], + adapters: ["0xAdapter1", "0xAdapter2"], + }; + const mockHashiMessage2: HashiMessage = { + nonce: 5, + targetChainId: 2, + threshold: 1, + sender: "0xSender", + receiver: "0xReceiver", + data: "0xData", + reporters: ["0xReporter1"], + adapters: ["0xAdapter1", "0xAdapter2"], + }; + beforeEach(() => { + mockToExecuteMesssage = jest.fn(); + mockExecuteMessage = jest.fn(); + }); + + it("should not increment nonce if no messages are executable", async () => { + mockToExecuteMesssage.mockResolvedValue(null); + + const result = await runHashiExecutor({ + chainId, + network, + nonce, + emitter: mockEmitter, + fetchBridgeConfig, + fetchVeaInbox, + isMessageExecutable: mockToExecuteMesssage, + executeMsgsOnHashi: mockExecuteMessage, + }); + expect(result).toBeDefined(); + expect(result).toBe(nonce); + }); + + it("should execute messages on Hashi if there are executable messages", async () => { + const executableNonces: VeaNonceToHashiMessage[] = []; + executableNonces.push({ + nonce: 1, + hashiMessage: mockHashiMessage1, + executed: false, + }); + executableNonces.push({ + nonce: 2, + hashiMessage: mockHashiMessage2, + executed: false, + }); + mockToExecuteMesssage + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(executableNonces[0]) + .mockResolvedValueOnce(executableNonces[1]); + + mockExecuteMessage.mockResolvedValueOnce(currentCount); + + const result = await runHashiExecutor({ + chainId, + network, + nonce, + emitter: mockEmitter, + fetchBridgeConfig, + fetchVeaInbox, + isMessageExecutable: mockToExecuteMesssage, + executeMsgsOnHashi: mockExecuteMessage, + }); + expect(result).toBeDefined(); + expect(mockExecuteMessage).toHaveBeenCalledWith(chainId, executableNonces); + expect(result).toBe(currentCount); + }); + }); +}); diff --git a/relayer-cli/src/utils/hashi.ts b/relayer-cli/src/utils/hashi.ts index 9003f2d3..99b0ebf0 100644 --- a/relayer-cli/src/utils/hashi.ts +++ b/relayer-cli/src/utils/hashi.ts @@ -1,56 +1,88 @@ -import { JsonRpcProvider, Interface, getAddress, zeroPadBytes, Contract, Wallet } from "ethers"; +import { EventEmitter } from "node:events"; +import { JsonRpcProvider, Interface, getAddress, Contract, Wallet, isHexString, getBytes } from "ethers"; import { getVeaMsgTrnx } from "./graphQueries"; import { getVeaInbox } from "./ethers"; import { getBridgeConfig } from "../consts/bridgeRoutes"; -import { executeMessagesAbi, messageDispatchedAbi, thresholdViewAbi } from "./hashiHelpers"; - -const SOURCE_CHAIN_ID = 421613; // Arbitrum Sepolia - -interface HashiMessage { - hashiContextNonce: number; - sender: string; - destinationChainId: number; - receiver: string; - threshold: number; - data: string; - reporters: string[]; - adapters: string[]; -} +import { getHashiMsgId } from "./hashiHelpers/hashiMsgUtils"; +import { messageDispatchedAbi, thresholdViewAbi, YaruAbi } from "./hashiHelpers/abi"; +import { BotEvents } from "./botEvents"; +import { HashiExecutionStatus, HashiMessage, VeaNonceToHashiMessage } from "./hashiHelpers/hashiTypes"; + +const SOURCE_CHAIN_ID = 421614; // Arbitrum Sepolia -type VeaNonceToHashiMessage = { +interface HashiExecutorInterface { + chainId: number; + network: string; nonce: number; - hashiMessage: HashiMessage; -}; + emitter: EventEmitter; + fetchBridgeConfig?: typeof getBridgeConfig; + fetchVeaInbox?: typeof getVeaInbox; + isMessageExecutable?: typeof toExecuteMesssage; + executeMsgsOnHashi?: typeof executeBatchOnHashi; +} -async function runHashiExecutor(chainId: number, network: string, nonce: number) { - const bridgeConfig = getBridgeConfig(chainId); +/** + * Run the Hashi executor to process and execute messages. + * @param chainId The chain ID + * @param network The network name + * @param nonce The starting Vea msg nonce + * @param emitter The event emitter + * @param fetchBridgeConfig Function to fetch bridge configuration + * @param fetchVeaInbox Function to fetch Vea Inbox contract instance + * @param isMessageExecutable Function to check if a message is executable + * @param executeMsgsOnHashi Function to execute messages on Hashi + * @returns The updated nonce after processing + */ +async function runHashiExecutor({ + chainId, + network, + nonce, + emitter, + fetchBridgeConfig = getBridgeConfig, + fetchVeaInbox = getVeaInbox, + isMessageExecutable = toExecuteMesssage, + executeMsgsOnHashi = executeBatchOnHashi, +}: HashiExecutorInterface): Promise { + const bridgeConfig = fetchBridgeConfig(chainId); const { veaContracts, rpcInbox } = bridgeConfig; const veaInboxAddress = veaContracts[network].veaInbox.address; const privateKey = process.env.PRIVATE_KEY; - const veaInbox = getVeaInbox(veaInboxAddress, privateKey, rpcInbox, chainId); + const veaInbox = fetchVeaInbox(veaInboxAddress, privateKey, rpcInbox, chainId); const inboxCount = await veaInbox.count(); const executableNonces: VeaNonceToHashiMessage[] = []; + const legacyNonce = nonce; while (nonce < inboxCount) { // ToDo: Add cooldown periods for nonces that cannot be executed. - const toExecute = await toExecuteMesssage(chainId, nonce, veaInboxAddress, rpcInbox); + const toExecute = await isMessageExecutable({ chainId, nonce, veaInboxAddress, rpcInbox }); if (toExecute) { executableNonces.push(toExecute); } nonce++; } - console.log(`Found ${executableNonces.length} executable nonces on Hashi`); - nonce = await executeBatchOnHashi(chainId, executableNonces); - console.log(`Processed up to nonce ${nonce} on Hashi`); + if (executableNonces.length === 0) { + return legacyNonce; + } + emitter.emit( + BotEvents.EXECUTING_HASHI, + executableNonces[0].nonce, + executableNonces[executableNonces.length - 1].nonce + ); + nonce = await executeMsgsOnHashi(chainId, executableNonces); + emitter.emit(BotEvents.HASHI_EXECUTED, nonce); return nonce; } +/** * Execute a batch of messages on Hashi (Yaru contract) + * @param chainId The chain ID + * @param params The array of VeaNonceToHashiMessage to execute + * @returns The last nonce processed + 1 + */ async function executeBatchOnHashi(chainId: number, params: VeaNonceToHashiMessage[]): Promise { const { yaruAddress, rpcOutbox } = getBridgeConfig(chainId); const maxPerTx = 20; const provider = new JsonRpcProvider(rpcOutbox); const signer = new Wallet(process.env.PRIVATE_KEY!, provider); - const yaru = new Contract(yaruAddress, executeMessagesAbi, signer); - const iface = new Interface(executeMessagesAbi); + const yaru = new Contract(yaruAddress, YaruAbi, signer); let cursor = 0; let lastNonceProcessed = 0; @@ -58,27 +90,83 @@ async function executeBatchOnHashi(chainId: number, params: VeaNonceToHashiMessa while (cursor < params.length) { const chunk = params.slice(cursor, cursor + maxPerTx); - const tx = await yaru.executeMessages(chunk); + const messages = chunk + .filter(({ executed }) => !executed) // only include unexecuted messages + .map(({ hashiMessage }) => { + const nonce = BigInt(hashiMessage.nonce); + const targetChainId = BigInt(hashiMessage.targetChainId); + const threshold = BigInt(hashiMessage.threshold); + + let dataBytes: string | Uint8Array = hashiMessage.data; + if (typeof dataBytes === "string") { + if (dataBytes === "" || dataBytes === "0x") { + dataBytes = new Uint8Array([]); + } else if (!isHexString(dataBytes)) { + dataBytes = getBytes(dataBytes); + } + } else { + // ensure it is a Uint8Array + dataBytes = new Uint8Array(dataBytes); + } + + const reporters = Array.isArray(hashiMessage.reporters) ? [...hashiMessage.reporters] : hashiMessage.reporters; + + const adapters = Array.isArray(hashiMessage.adapters) ? [...hashiMessage.adapters] : hashiMessage.adapters; + + return { + nonce, + targetChainId, + threshold, + sender: hashiMessage.sender, + receiver: hashiMessage.receiver, + data: dataBytes, + reporters, + adapters, + }; + }); + + const tx = await yaru.executeMessages(messages); const receipt = await tx.wait(); cursor += chunk.length; lastNonceProcessed = Number(params[Math.min(cursor, params.length) - 1].nonce); - - console.log(`Executed ${chunk.length} messages in tx ${receipt.transactionHash}`); } return ++lastNonceProcessed; } -async function toExecuteMesssage( - chainId: number, - nonce: number, - inboxAddress: string, - rpcInbox: string -): Promise { - const hashes = await getVeaMsgTrnx(nonce, inboxAddress); - const provider = new JsonRpcProvider(rpcInbox); - const bridgeConfig = getBridgeConfig(chainId); +interface ToExecuteMessageInterface { + chainId: number; + nonce: number; + veaInboxAddress: string; + rpcInbox: string; + fetchVeaMsgTrnx?: typeof getVeaMsgTrnx; + fetchBridgeConfig?: typeof getBridgeConfig; + hasThresholdMet?: typeof getMessageStatus; + provider?: JsonRpcProvider; + logIFace?: Interface; +} +/** + * Check if a message is executable on Hashi by verifying if the threshold is met. + * @param chainId The chain ID + * @param nonce The message nonce + * @param inboxAddress The Vea Inbox address + * @param rpcInbox The RPC URL for the inbox network + * @returns The VeaNonceToHashiMessage if executable, otherwise null + */ +async function toExecuteMesssage({ + chainId, + nonce, + veaInboxAddress, + rpcInbox, + fetchVeaMsgTrnx = getVeaMsgTrnx, + fetchBridgeConfig = getBridgeConfig, + hasThresholdMet = getMessageStatus, + provider = new JsonRpcProvider(rpcInbox), + logIFace = new Interface(messageDispatchedAbi), +}: ToExecuteMessageInterface): Promise { + const hashes = await fetchVeaMsgTrnx(nonce, veaInboxAddress); + const bridgeConfig = fetchBridgeConfig(chainId); const yahoAddress = bridgeConfig.yahoAddress?.toLowerCase(); const receipt = await provider.getTransactionReceipt(hashes[0]); let executeNonce: VeaNonceToHashiMessage | null = null; @@ -86,23 +174,24 @@ async function toExecuteMesssage( for (const log of receipt.logs) { if (log.address.toLowerCase() === yahoAddress) { - const logIFace = new Interface(messageDispatchedAbi); const logData = log.data; const decodedLog = logIFace.decodeEventLog("MessageDispatched", logData, log.topics); const message = decodedLog.message; hashiMessage = { - hashiContextNonce: message[0], - destinationChainId: message[1], + nonce: message[0], + targetChainId: message[1], threshold: message[2], sender: message[3], receiver: message[4], data: message[5], - reporters: message[6], - adapters: message[7], + reporters: message[6] as string[], + adapters: message[7] as string[], }; - const thresholdMet = await isThresholdMet(hashiMessage); - if (thresholdMet) { - executeNonce = { nonce, hashiMessage }; + const msgStatus = await hasThresholdMet(hashiMessage); + if (msgStatus === HashiExecutionStatus.EXECUTABLE) { + executeNonce = { nonce, hashiMessage, executed: false }; + } else if (msgStatus === HashiExecutionStatus.EXECUTED) { + executeNonce = { nonce, hashiMessage, executed: true }; } break; } @@ -110,19 +199,39 @@ async function toExecuteMesssage( return executeNonce; } -async function isThresholdMet(message: HashiMessage): Promise { - const bridgeConfig = getBridgeConfig(message.destinationChainId); +/** * Get the message status for threshold and execution on Hashi. + * @param message The HashiMessage to check + * @returns The HashiExecutionStatus indicating if the message is executable or already executed + */ +async function getMessageStatus(message: HashiMessage): Promise { + const bridgeConfig = getBridgeConfig(message.targetChainId); const hashiAddress = bridgeConfig.hashiAddress; + const yaruAddress = bridgeConfig.yaruAddress; const provider = new JsonRpcProvider(bridgeConfig.rpcOutbox); + + // Check if msg is already executed + const ifaceYaru = new Interface(YaruAbi); const domain = BigInt(SOURCE_CHAIN_ID); // uint256 - const id = BigInt(message.hashiContextNonce); // or messageId if contract expects that + const id = getHashiMsgId(SOURCE_CHAIN_ID, bridgeConfig.yahoAddress!, message); // bytes32 const threshold = BigInt(message.threshold); // uint256 const adapters = message.adapters.map((address) => getAddress(address)); // address[] const iface = new Interface(thresholdViewAbi); - const data = iface.encodeFunctionData("checkHashWithThresholdFromAdapters", [domain, id, threshold, adapters]); + const data = iface.encodeFunctionData("checkHashWithThresholdFromAdapters", [ + domain, + id, + BigInt(threshold), + adapters, + ]); const ret = await provider.call({ to: hashiAddress, data }); const [ok] = iface.decodeFunctionResult("checkHashWithThresholdFromAdapters", ret); - return ok; + if (ok) { + const executionData = ifaceYaru.encodeFunctionData("executed", [id]); + const ret = await provider.call({ to: yaruAddress, data: executionData }); + const [flag] = ifaceYaru.decodeFunctionResult("executed", ret); + console.log("Message already executed flag:", flag); + if (flag) return HashiExecutionStatus.EXECUTED; // already executed + } + return ok ? HashiExecutionStatus.EXECUTABLE : HashiExecutionStatus.THRESHOLD_NOT_MET; } export { executeBatchOnHashi, runHashiExecutor, toExecuteMesssage }; diff --git a/relayer-cli/src/utils/hashiHelpers.ts b/relayer-cli/src/utils/hashiHelpers.ts deleted file mode 100644 index 4371686a..00000000 --- a/relayer-cli/src/utils/hashiHelpers.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const dispatchAbi = [ - "function dispatchMessageToAdapters(uint256 targetChainId,uint256 threshold,address receiver,bytes data,address[] reporters,address[] adapters)", -]; - -export const messageDispatchedAbi = [ - "event MessageDispatched(uint256 indexed messageId, (uint256 nonce,uint256 targetChainId,uint256 threshold,address sender,address receiver,bytes data,address[] reporters,address[] adapters) message)", -]; - -export const thresholdViewAbi = [ - "function checkHashWithThresholdFromAdapters(uint256 domain,uint256 id,uint256 threshold,address[] adapters) view returns (bool)", -]; - -export const executeMessagesAbi = [ - "function executeMessages((uint256,uint256,uint256,address,address,bytes,address[],address[])[] messages) external returns (bytes[])", -]; diff --git a/relayer-cli/src/utils/hashiHelpers/abi.ts b/relayer-cli/src/utils/hashiHelpers/abi.ts new file mode 100644 index 00000000..7b455934 --- /dev/null +++ b/relayer-cli/src/utils/hashiHelpers/abi.ts @@ -0,0 +1,342 @@ +export const dispatchAbi = [ + "function dispatchMessageToAdapters(uint256 targetChainId,uint256 threshold,address receiver,bytes data,address[] reporters,address[] adapters)", +]; + +export const messageDispatchedAbi = [ + "event MessageDispatched(uint256 indexed messageId, (uint256 nonce,uint256 targetChainId,uint256 threshold,address sender,address receiver,bytes data,address[] reporters,address[] adapters) message)", +]; + +export const thresholdViewAbi = [ + "function checkHashWithThresholdFromAdapters(uint256 domain,uint256 id,uint256 threshold,address[] adapters) view returns (bool)", +]; + +export const YaruAbi = [ + { + inputs: [ + { + internalType: "address", + name: "hashi", + type: "address", + }, + { + internalType: "address", + name: "yaho_", + type: "address", + }, + { + internalType: "uint256", + name: "sourceChainId", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "CallFailed", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "chainId", + type: "uint256", + }, + { + internalType: "uint256", + name: "expectedChainId", + type: "uint256", + }, + ], + name: "InvalidToChainId", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "messageId", + type: "uint256", + }, + ], + name: "MessageIdAlreadyExecuted", + type: "error", + }, + { + inputs: [], + name: "ThresholdNotMet", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "messageId", + type: "uint256", + }, + { + components: [ + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + { + internalType: "uint256", + name: "targetChainId", + type: "uint256", + }, + { + internalType: "uint256", + name: "threshold", + type: "uint256", + }, + { + internalType: "address", + name: "sender", + type: "address", + }, + { + internalType: "address", + name: "receiver", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + internalType: "contract IReporter[]", + name: "reporters", + type: "address[]", + }, + { + internalType: "contract IAdapter[]", + name: "adapters", + type: "address[]", + }, + ], + indexed: false, + internalType: "struct Message", + name: "message", + type: "tuple", + }, + ], + name: "MessageExecuted", + type: "event", + }, + { + inputs: [], + name: "HASHI", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "SOURCE_CHAIN_ID", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "YAHO", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + { + internalType: "uint256", + name: "targetChainId", + type: "uint256", + }, + { + internalType: "uint256", + name: "threshold", + type: "uint256", + }, + { + internalType: "address", + name: "sender", + type: "address", + }, + { + internalType: "address", + name: "receiver", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + internalType: "contract IReporter[]", + name: "reporters", + type: "address[]", + }, + { + internalType: "contract IAdapter[]", + name: "adapters", + type: "address[]", + }, + ], + internalType: "struct Message", + name: "message", + type: "tuple", + }, + ], + name: "calculateMessageHash", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "sourceChainId", + type: "uint256", + }, + { + internalType: "address", + name: "dispatcherAddress", + type: "address", + }, + { + internalType: "bytes32", + name: "messageHash", + type: "bytes32", + }, + ], + name: "calculateMessageId", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + { + internalType: "uint256", + name: "targetChainId", + type: "uint256", + }, + { + internalType: "uint256", + name: "threshold", + type: "uint256", + }, + { + internalType: "address", + name: "sender", + type: "address", + }, + { + internalType: "address", + name: "receiver", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + internalType: "contract IReporter[]", + name: "reporters", + type: "address[]", + }, + { + internalType: "contract IAdapter[]", + name: "adapters", + type: "address[]", + }, + ], + internalType: "struct Message[]", + name: "messages", + type: "tuple[]", + }, + ], + name: "executeMessages", + outputs: [ + { + internalType: "bytes[]", + name: "", + type: "bytes[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "executed", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, +]; diff --git a/relayer-cli/src/utils/hashiHelpers/hashiMsgUtils.ts b/relayer-cli/src/utils/hashiHelpers/hashiMsgUtils.ts new file mode 100644 index 00000000..26ab6a22 --- /dev/null +++ b/relayer-cli/src/utils/hashiHelpers/hashiMsgUtils.ts @@ -0,0 +1,64 @@ +import { keccak256, AbiCoder, getBytes, isHexString, getAddress, toBeArray } from "ethers"; +import type { HashiMessage } from "./hashiTypes"; + +const coder = AbiCoder.defaultAbiCoder(); + +export function calculateMessageHash(message: HashiMessage): string { + const encoded = encodeMessageForAbi(message); + return keccak256(encoded); +} + +export function getHashiMsgId( + sourceChainId: bigint | number | string, + dispatcherAddress: string, + message: HashiMessage +): BigInt { + // Normalize exactly as Solidity would see inputs + const source = BigInt(sourceChainId); + const dispatcher = getAddress(dispatcherAddress); // checksum normalize + + const msgHash = calculateMessageHash(message); + const encodedId = coder.encode(["uint256", "address", "bytes32"], [source, dispatcher, msgHash]); + return hexToBigInt32Bytes(keccak256(encodedId)); +} + +function encodeMessageForAbi(message: HashiMessage): string { + // Strict normalization + const nonce = BigInt(message.nonce); + const targetChainId = BigInt(message.targetChainId); + const threshold = BigInt(message.threshold); + + const sender = getAddress(message.sender); + const receiver = getAddress(message.receiver); + + const data = + typeof message.data === "string" + ? isHexString(message.data) + ? message.data + : (() => { + throw new Error("message.data must be 0x-hex"); + })() + : getBytes(message.data); + + // Preserve array order and checksum-normalize each address + const reporters = message.reporters.map(getAddress); + const adapters = message.adapters.map(getAddress); + + // Exact struct layout as tuple for abi.encode(message) + return coder.encode( + [ + "tuple(uint256 nonce,uint256 targetChainId,uint256 threshold,address sender,address receiver,bytes data,address[] reporters,address[] adapters)", + ], + [{ nonce, targetChainId, threshold, sender, receiver, data, reporters, adapters }] + ); +} + +function hexToBigInt32Bytes(hex32: string): bigint { + const bytes = toBeArray(hex32); + // Convert bytes to BigInt (big-endian) + let n = BigInt(0); + for (const b of Array.from(bytes)) { + n = (n << BigInt(8)) + BigInt(b); + } + return n; +} diff --git a/relayer-cli/src/utils/hashiHelpers/hashiTypes.ts b/relayer-cli/src/utils/hashiHelpers/hashiTypes.ts new file mode 100644 index 00000000..0138c593 --- /dev/null +++ b/relayer-cli/src/utils/hashiHelpers/hashiTypes.ts @@ -0,0 +1,22 @@ +export interface HashiMessage { + nonce: number; + sender: string; + targetChainId: number; + receiver: string; + threshold: number; + data: string; + reporters: string[]; + adapters: string[]; +} + +export type VeaNonceToHashiMessage = { + nonce: number; + hashiMessage: HashiMessage; + executed: boolean; +}; + +export enum HashiExecutionStatus { + THRESHOLD_NOT_MET = "THRESHOLD_NOT_MET", + EXECUTABLE = "EXECUTABLE", + EXECUTED = "EXECUTED", +} diff --git a/relayer-cli/src/utils/logger.ts b/relayer-cli/src/utils/logger.ts index c676b97a..0597d27b 100644 --- a/relayer-cli/src/utils/logger.ts +++ b/relayer-cli/src/utils/logger.ts @@ -59,4 +59,15 @@ export const configurableInitialize = (emitter: EventEmitter) => { emitter.on(BotEvents.MESSAGE_EXECUTION_FAILED, (nonce) => { console.error(`Message execution failed for nonce ${nonce}`); }); + + // Hashi executor logs + emitter.on(BotEvents.EXECUTING_HASHI, (startNonce, endNonce) => { + console.log(`Executing Hashi for nonces from ${startNonce} to ${endNonce}`); + }); + emitter.on(BotEvents.HASHI_EXECUTED, (endNonce) => { + console.log(`Successfully executed Hashi till ${endNonce}`); + }); + emitter.on(BotEvents.HASHI_BATCH_TXN, (txHash, batchSize) => { + console.log(`Hashi batch transaction ${txHash} for ${batchSize} messages`); + }); }; From e1bbda46cf054c6ef39deca453ca3943d3e64c60 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 22 Oct 2025 18:44:04 +0530 Subject: [PATCH 07/10] chore: refactor gql queries --- relayer-cli/src/utils/graphQueries.ts | 69 ++++++++++++++++++++++++++- relayer-cli/src/utils/relay.ts | 69 +-------------------------- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/relayer-cli/src/utils/graphQueries.ts b/relayer-cli/src/utils/graphQueries.ts index 62b4c901..afa4655d 100644 --- a/relayer-cli/src/utils/graphQueries.ts +++ b/relayer-cli/src/utils/graphQueries.ts @@ -1,4 +1,5 @@ import request from "graphql-request"; +import { VeaOutboxArbToEth, VeaOutboxArbToGnosis } from "@kleros/vea-contracts/typechain-types"; async function getVeaMsgTrnx(nonce: number, inboxAddress: string) { console.log(`Fetching transaction hashes for nonce ${nonce} from inbox ${inboxAddress}`); @@ -18,4 +19,70 @@ async function getVeaMsgTrnx(nonce: number, inboxAddress: string) { } } -export { getVeaMsgTrnx }; +interface SnapshotResponse { + snapshotSaveds: Array<{ count: string }>; +} +/** + * Get the count of the veaOutbox + * @param veaOutbox The veaOutbox contract instance + * @param chainId The chain id of the veaOutbox chain + * @returns The count of the veaOutbox + */ +const getCount = async (veaOutbox: VeaOutboxArbToEth | VeaOutboxArbToGnosis, chainId: number): Promise => { + const subgraph = process.env.RELAYER_SUBGRAPH; + const stateRoot = await veaOutbox.stateRoot(); + + const result = (await request( + `https://api.studio.thegraph.com/query/${subgraph}`, + `{ + snapshotSaveds(first: 1, where: { stateRoot: "${stateRoot}" }) { + count + } + }` + )) as SnapshotResponse; + + if (result["snapshotSaveds"].length == 0) return 0; + + return Number(result["snapshotSaveds"][0].count); +}; + +interface MessageSent { + nonce: number; +} + +interface MessageSentsResponse { + messageSents: MessageSent[]; +} + +/** + * Get the nonces of messages sent by a given sender + * @param chainId The chain id of the veaOutbox chain + * @param nonce The nonce of the first message to relay + * @param msgSender The address of the sender + * @returns The nonces of the messages sent by the sender + */ +const getNonceFrom = async (chainId: number, inbox: string, nonce: number, msgSender: string) => { + const subgraph = process.env.RELAYER_SUBGRAPH; + + const result = (await request( + `https://api.studio.thegraph.com/query/${subgraph}`, + `{ + messageSents( + first: 1000, + where: { + inbox: "${inbox}", + nonce_gte: ${nonce}, + msgSender_: {id: "${msgSender}"} + }, + orderBy: nonce, + orderDirection: asc + ) { + nonce + } + }` + )) as MessageSentsResponse; + + return result[`messageSents`].map((a: { nonce: number }) => a.nonce); +}; + +export { getVeaMsgTrnx, getCount, getNonceFrom }; diff --git a/relayer-cli/src/utils/relay.ts b/relayer-cli/src/utils/relay.ts index 92123a49..7ee0a5c5 100644 --- a/relayer-cli/src/utils/relay.ts +++ b/relayer-cli/src/utils/relay.ts @@ -1,40 +1,12 @@ require("dotenv").config(); -import request from "graphql-request"; import { EventEmitter } from "node:events"; -import { VeaOutboxArbToEth, VeaOutboxArbToGnosis } from "@kleros/vea-contracts/typechain-types"; +import { getCount, getNonceFrom } from "./graphQueries"; import { getProofAtCount, getMessageDataToRelay } from "./proof"; import { getVeaOutbox, getBatcher } from "./ethers"; import { getBridgeConfig, Network } from "../consts/bridgeRoutes"; import { BotEvents } from "./botEvents"; import { MissingEnvironmentVariable, InvalidChainId, DataError } from "./errors"; -interface SnapshotResponse { - snapshotSaveds: Array<{ count: string }>; -} -/** - * Get the count of the veaOutbox - * @param veaOutbox The veaOutbox contract instance - * @param chainId The chain id of the veaOutbox chain - * @returns The count of the veaOutbox - */ -const getCount = async (veaOutbox: VeaOutboxArbToEth | VeaOutboxArbToGnosis, chainId: number): Promise => { - const subgraph = process.env.RELAYER_SUBGRAPH; - const stateRoot = await veaOutbox.stateRoot(); - - const result = (await request( - `https://api.studio.thegraph.com/query/${subgraph}`, - `{ - snapshotSaveds(first: 1, where: { stateRoot: "${stateRoot}" }) { - count - } - }` - )) as SnapshotResponse; - - if (result["snapshotSaveds"].length == 0) return 0; - - return Number(result["snapshotSaveds"][0].count); -}; - /** * Relay a message from the veaOutbox * @param chainId The chain id of the veaOutbox chain @@ -213,43 +185,4 @@ const relayAllFrom = async ( return lastNonce + 1; // return current nonce }; -interface MessageSent { - nonce: number; -} - -interface MessageSentsResponse { - messageSents: MessageSent[]; -} - -/** - * Get the nonces of messages sent by a given sender - * @param chainId The chain id of the veaOutbox chain - * @param nonce The nonce of the first message to relay - * @param msgSender The address of the sender - * @returns The nonces of the messages sent by the sender - */ -const getNonceFrom = async (chainId: number, inbox: string, nonce: number, msgSender: string) => { - const subgraph = process.env.RELAYER_SUBGRAPH; - - const result = (await request( - `https://api.studio.thegraph.com/query/${subgraph}`, - `{ - messageSents( - first: 1000, - where: { - inbox: "${inbox}", - nonce_gte: ${nonce}, - msgSender_: {id: "${msgSender}"} - }, - orderBy: nonce, - orderDirection: asc - ) { - nonce - } - }` - )) as MessageSentsResponse; - - return result[`messageSents`].map((a: { nonce: number }) => a.nonce); -}; - export { relayAllFrom, relay, relayBatch, RelayBatchDeps }; From 1f520860fdc72d3918fcc0406c0ea5b2f19d6b31 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 22 Oct 2025 18:44:22 +0530 Subject: [PATCH 08/10] chore: add chiado hashi support --- relayer-cli/src/consts/bridgeRoutes.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/relayer-cli/src/consts/bridgeRoutes.ts b/relayer-cli/src/consts/bridgeRoutes.ts index ed87caec..bd599c68 100644 --- a/relayer-cli/src/consts/bridgeRoutes.ts +++ b/relayer-cli/src/consts/bridgeRoutes.ts @@ -68,7 +68,7 @@ const bridges: { [chainId: number]: IBridge } = { rpcOutbox: process.env.RPC_SEPOLIA!, yahoAddress: "0xDbdF80c87f414fac8342e04D870764197bD3bAC7", // Hashi (Yaho) contract address on Arbitrum Sepolia yaruAddress: "0x231e48AAEaAC6398978a1dBA4Cd38fcA208Ec391", // Hashi (Yaru) contract address on Sepolia - hashiAddress: "0x78E4ae687De18B3B71Ccd0e8a3A76Fed49a02A02", // Hashi (Yaru) contract address on Sepolia + hashiAddress: "0x78E4ae687De18B3B71Ccd0e8a3A76Fed49a02A02", // Hashi (Hashi) contract address on Sepolia }, 10200: { chainId: 10200, @@ -78,6 +78,9 @@ const bridges: { [chainId: number]: IBridge } = { batcherAddress: process.env.TRANSACTION_BATCHER_CONTRACT_CHIADO!, rpcInbox: process.env.RPC_ARBITRUM_SEPOLIA!, rpcOutbox: process.env.RPC_CHIADO!, + yahoAddress: "0xDbdF80c87f414fac8342e04D870764197bD3bAC7", // Hashi (Yaho) contract address on Arbitrum Sepolia + yaruAddress: "0x639c26C9F45C634dD14C599cBAa27363D4665C53", // Hashi (Yaru) contract address on Chiado + hashiAddress: "0x78E4ae687De18B3B71Ccd0e8a3A76Fed49a02A02", // Hashi (Hashi) contract address on Chiado }, }; From 05689657a171d0748089ea9aeb40aa253d9818c9 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 27 Oct 2025 19:15:19 +0530 Subject: [PATCH 09/10] chore: bot review fixes --- relayer-cli/src/utils/graphQueries.ts | 4 ++-- relayer-cli/src/utils/hashi.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/relayer-cli/src/utils/graphQueries.ts b/relayer-cli/src/utils/graphQueries.ts index afa4655d..95e81c15 100644 --- a/relayer-cli/src/utils/graphQueries.ts +++ b/relayer-cli/src/utils/graphQueries.ts @@ -72,7 +72,7 @@ const getNonceFrom = async (chainId: number, inbox: string, nonce: number, msgSe where: { inbox: "${inbox}", nonce_gte: ${nonce}, - msgSender_: {id: "${msgSender}"} + msgSender_: {id: "${msgSender.toLowerCase()}"} }, orderBy: nonce, orderDirection: asc @@ -82,7 +82,7 @@ const getNonceFrom = async (chainId: number, inbox: string, nonce: number, msgSe }` )) as MessageSentsResponse; - return result[`messageSents`].map((a: { nonce: number }) => a.nonce); + return result[`messageSents`].map((a: { nonce: string | number }) => Number(a.nonce)); }; export { getVeaMsgTrnx, getCount, getNonceFrom }; diff --git a/relayer-cli/src/utils/hashi.ts b/relayer-cli/src/utils/hashi.ts index 99b0ebf0..5344c14a 100644 --- a/relayer-cli/src/utils/hashi.ts +++ b/relayer-cli/src/utils/hashi.ts @@ -125,6 +125,11 @@ async function executeBatchOnHashi(chainId: number, params: VeaNonceToHashiMessa }; }); + if (messages.length === 0) { + cursor += chunk.length; + continue; + } + const tx = await yaru.executeMessages(messages); const receipt = await tx.wait(); From 0e4ca91850ddd7f11689b452137f0802a0f6bced Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 27 Oct 2025 19:20:05 +0530 Subject: [PATCH 10/10] fix: typo --- relayer-cli/src/utils/hashi.test.ts | 20 ++++++++++---------- relayer-cli/src/utils/hashi.ts | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/relayer-cli/src/utils/hashi.test.ts b/relayer-cli/src/utils/hashi.test.ts index b00f0ffc..026463e0 100644 --- a/relayer-cli/src/utils/hashi.test.ts +++ b/relayer-cli/src/utils/hashi.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; import { BotEvents } from "./botEvents"; -import { toExecuteMesssage, runHashiExecutor } from "./hashi"; +import { toExecuteMessage, runHashiExecutor } from "./hashi"; import { VeaNonceToHashiMessage, HashiExecutionStatus, HashiMessage } from "./hashiHelpers/hashiTypes"; class MockEmitter extends EventEmitter { @@ -100,7 +100,7 @@ describe("hashi", () => { }); describe("toExecuteMessage", () => { it("should verify if the vea message is executed using Hashi and is executable", async () => { - const result = await toExecuteMesssage({ + const result = await toExecuteMessage({ chainId, nonce, veaInboxAddress: "0xInbox", @@ -116,7 +116,7 @@ describe("hashi", () => { expect(result?.hashiMessage.nonce).toBe(BigInt(1)); }); it("should return null if the vea message is not executable via Hashi", async () => { - const result = await toExecuteMesssage({ + const result = await toExecuteMessage({ chainId, nonce, veaInboxAddress: "0xInbox", @@ -134,7 +134,7 @@ describe("hashi", () => { getTransactionReceipt: jest.fn().mockResolvedValue({ logs: [] }), } as any; - const result = await toExecuteMesssage({ + const result = await toExecuteMessage({ chainId, nonce, veaInboxAddress: "0xInbox", @@ -150,7 +150,7 @@ describe("hashi", () => { }); describe("runHashiExecutor", () => { - let mockToExecuteMesssage: jest.Mock; + let mocktoExecuteMessage: jest.Mock; let mockExecuteMessage: jest.Mock; const mockHashiMessage1: HashiMessage = { nonce: 3, @@ -173,12 +173,12 @@ describe("hashi", () => { adapters: ["0xAdapter1", "0xAdapter2"], }; beforeEach(() => { - mockToExecuteMesssage = jest.fn(); + mocktoExecuteMessage = jest.fn(); mockExecuteMessage = jest.fn(); }); it("should not increment nonce if no messages are executable", async () => { - mockToExecuteMesssage.mockResolvedValue(null); + mocktoExecuteMessage.mockResolvedValue(null); const result = await runHashiExecutor({ chainId, @@ -187,7 +187,7 @@ describe("hashi", () => { emitter: mockEmitter, fetchBridgeConfig, fetchVeaInbox, - isMessageExecutable: mockToExecuteMesssage, + isMessageExecutable: mocktoExecuteMessage, executeMsgsOnHashi: mockExecuteMessage, }); expect(result).toBeDefined(); @@ -206,7 +206,7 @@ describe("hashi", () => { hashiMessage: mockHashiMessage2, executed: false, }); - mockToExecuteMesssage + mocktoExecuteMessage .mockResolvedValueOnce(null) .mockResolvedValueOnce(executableNonces[0]) .mockResolvedValueOnce(executableNonces[1]); @@ -220,7 +220,7 @@ describe("hashi", () => { emitter: mockEmitter, fetchBridgeConfig, fetchVeaInbox, - isMessageExecutable: mockToExecuteMesssage, + isMessageExecutable: mocktoExecuteMessage, executeMsgsOnHashi: mockExecuteMessage, }); expect(result).toBeDefined(); diff --git a/relayer-cli/src/utils/hashi.ts b/relayer-cli/src/utils/hashi.ts index 5344c14a..81d69905 100644 --- a/relayer-cli/src/utils/hashi.ts +++ b/relayer-cli/src/utils/hashi.ts @@ -17,7 +17,7 @@ interface HashiExecutorInterface { emitter: EventEmitter; fetchBridgeConfig?: typeof getBridgeConfig; fetchVeaInbox?: typeof getVeaInbox; - isMessageExecutable?: typeof toExecuteMesssage; + isMessageExecutable?: typeof toExecuteMessage; executeMsgsOnHashi?: typeof executeBatchOnHashi; } @@ -40,7 +40,7 @@ async function runHashiExecutor({ emitter, fetchBridgeConfig = getBridgeConfig, fetchVeaInbox = getVeaInbox, - isMessageExecutable = toExecuteMesssage, + isMessageExecutable = toExecuteMessage, executeMsgsOnHashi = executeBatchOnHashi, }: HashiExecutorInterface): Promise { const bridgeConfig = fetchBridgeConfig(chainId); @@ -159,7 +159,7 @@ interface ToExecuteMessageInterface { * @param rpcInbox The RPC URL for the inbox network * @returns The VeaNonceToHashiMessage if executable, otherwise null */ -async function toExecuteMesssage({ +async function toExecuteMessage({ chainId, nonce, veaInboxAddress, @@ -239,4 +239,4 @@ async function getMessageStatus(message: HashiMessage): Promise