diff --git a/.changeset/lovely-planes-end.md b/.changeset/lovely-planes-end.md new file mode 100644 index 0000000000..81bd6a8598 --- /dev/null +++ b/.changeset/lovely-planes-end.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/cli': minor +'@hyperlane-xyz/sdk': minor +--- + +Allow self-relaying of all messages if there are multiple in a given dispatch transaction. diff --git a/typescript/cli/src/commands/status.ts b/typescript/cli/src/commands/status.ts index 2b4c204880..0875b64675 100644 --- a/typescript/cli/src/commands/status.ts +++ b/typescript/cli/src/commands/status.ts @@ -19,12 +19,11 @@ export const statusCommand: CommandModuleWithWriteContext< description: 'Dispatch transaction hash', }, }, - handler: async ({ context, origin, destination, id, relay, dispatchTx }) => { + handler: async ({ context, origin, id, relay, dispatchTx }) => { await checkMessageStatus({ context, dispatchTx, messageId: id, - destination, origin, selfRelay: relay, }); diff --git a/typescript/cli/src/status/message.ts b/typescript/cli/src/status/message.ts index 5e22aec7b0..b5051b4c92 100644 --- a/typescript/cli/src/status/message.ts +++ b/typescript/cli/src/status/message.ts @@ -2,17 +2,15 @@ import type { TransactionReceipt } from '@ethersproject/providers'; import { input } from '@inquirer/prompts'; import { ChainName, HyperlaneCore, HyperlaneRelayer } from '@hyperlane-xyz/sdk'; -import { assert, parseWarpRouteMessage } from '@hyperlane-xyz/utils'; import { WriteCommandContext } from '../context/types.js'; -import { log, logBlue, logGray, logGreen, logRed } from '../logger.js'; +import { log, logBlue, logGreen, logRed } from '../logger.js'; import { runSingleChainSelectionStep } from '../utils/chains.js'; import { stubMerkleTreeConfig } from '../utils/relay.js'; export async function checkMessageStatus({ context, messageId, - destination, origin, selfRelay, dispatchTx, @@ -20,7 +18,6 @@ export async function checkMessageStatus({ context: WriteCommandContext; dispatchTx?: string; messageId?: string; - destination?: ChainName; origin?: ChainName; selfRelay?: boolean; }) { @@ -31,15 +28,9 @@ export async function checkMessageStatus({ ); } - if (!messageId) { - messageId = await input({ - message: 'Please specify the message id', - }); - } - - const chainAddresses = await context.registry.getAddresses(); + const coreAddresses = await context.registry.getAddresses(); const core = HyperlaneCore.fromAddressesMap( - chainAddresses, + coreAddresses, context.multiProvider, ); @@ -50,6 +41,9 @@ export async function checkMessageStatus({ .getProvider(origin) .getTransactionReceipt(dispatchTx); } else { + messageId ??= await input({ + message: 'Please specify the message id', + }); try { dispatchedReceipt = await core.getDispatchTx(origin, messageId); } catch { @@ -64,48 +58,29 @@ export async function checkMessageStatus({ } } - const messages = core.getDispatchedMessages(dispatchedReceipt!); - const match = messages.find((m) => m.id === messageId); - assert(match, `Message ${messageId} not found in dispatch tx ${dispatchTx}`); - const message = match; - try { - const { amount, recipient } = parseWarpRouteMessage(message.parsed.body); - logGray(`Warping ${amount} to ${recipient}`); - // eslint-disable-next-line no-empty - } catch {} - - let deliveredTx: TransactionReceipt; - - log(`Checking status of message ${messageId} on ${destination}`); - const delivered = await core.isDelivered(message); - if (delivered) { - logGreen(`Message ${messageId} was delivered`); - deliveredTx = await core.getProcessedReceipt(message); - } else { - logBlue(`Message ${messageId} was not yet delivered`); + const messages = core.getDispatchedMessages(dispatchedReceipt); - if (!selfRelay) { - return; + const undelivered = []; + for (const message of messages) { + log( + `Checking status of message ${message.id} on ${message.parsed.destinationChain}`, + ); + const delivered = await core.isDelivered(message); + if (delivered) { + logGreen(`Message ${message.id} was delivered`); + } else { + logBlue(`Message ${message.id} was not yet delivered`); + undelivered.push(message); } + } + if (selfRelay) { const relayer = new HyperlaneRelayer({ core }); - - const hookAddress = await core.getSenderHookAddress(message); - const merkleAddress = chainAddresses[origin].merkleTreeHook; - stubMerkleTreeConfig(relayer, origin, hookAddress, merkleAddress); - - deliveredTx = await relayer.relayMessage( - dispatchedReceipt, - undefined, - message, - ); + for (const message of undelivered) { + const hookAddress = await core.getSenderHookAddress(message); + const merkleAddress = coreAddresses[origin].merkleTreeHook; + stubMerkleTreeConfig(relayer, origin, hookAddress, merkleAddress); + } + await relayer.relayAll(dispatchedReceipt, undelivered); } - - logGreen( - `Message ${messageId} delivered in ${ - context.multiProvider.tryGetExplorerTxUrl(message.parsed.destination, { - hash: deliveredTx.transactionHash, - }) ?? deliveredTx.transactionHash - }`, - ); } diff --git a/typescript/sdk/src/core/HyperlaneCore.ts b/typescript/sdk/src/core/HyperlaneCore.ts index a3130ecd1a..10d4415ee7 100644 --- a/typescript/sdk/src/core/HyperlaneCore.ts +++ b/typescript/sdk/src/core/HyperlaneCore.ts @@ -14,6 +14,7 @@ import { ProtocolType, addBufferToGasLimit, addressToBytes32, + assert, bytes32ToAddress, isZeroishAddress, messageId, @@ -313,7 +314,7 @@ export class HyperlaneCore extends HyperlaneApp { message: DispatchedMessage, ): Promise { const destinationChain = this.getDestination(message); - const mailbox = this.contractsMap[destinationChain].mailbox; + const mailbox = this.getContracts(destinationChain).mailbox; const processedBlock = await mailbox.processedAt(message.id); const events = await mailbox.queryFilter( @@ -321,6 +322,11 @@ export class HyperlaneCore extends HyperlaneApp { processedBlock, processedBlock, ); + + assert( + events.length === 1, + `Expected exactly one process event, got ${events.length}`, + ); const processedEvent = events[0]; return processedEvent.getTransactionReceipt(); } @@ -428,6 +434,8 @@ export class HyperlaneCore extends HyperlaneApp { if (matching.length === 0) { throw new Error(`No dispatch event found for message ${messageId}`); } + + assert(matching.length === 1, 'Multiple dispatch events found'); const event = matching[0]; // only 1 event per message ID return event.getTransactionReceipt(); } diff --git a/typescript/sdk/src/core/HyperlaneRelayer.ts b/typescript/sdk/src/core/HyperlaneRelayer.ts index 9b82f90cbb..c36a37aef3 100644 --- a/typescript/sdk/src/core/HyperlaneRelayer.ts +++ b/typescript/sdk/src/core/HyperlaneRelayer.ts @@ -207,6 +207,38 @@ export class HyperlaneRelayer { return this.getIsmConfig(destinationChain, ism, message); } + async relayAll( + dispatchTx: providers.TransactionReceipt, + messages = HyperlaneCore.getDispatchedMessages(dispatchTx), + ): Promise> { + const destinationMap: ChainMap = {}; + messages.forEach((message) => { + destinationMap[message.parsed.destination] ??= []; + destinationMap[message.parsed.destination].push(message); + }); + + // parallelize relaying to different destinations + return promiseObjAll( + objMap(destinationMap, async (_destination, messages) => { + const receipts: ethers.ContractReceipt[] = []; + // serially relay messages to the same destination + for (const message of messages) { + try { + const receipt = await this.relayMessage( + dispatchTx, + undefined, + message, + ); + receipts.push(receipt); + } catch (e) { + this.logger.error(`Failed to relay message ${message.id}, ${e}`); + } + } + return receipts; + }), + ); + } + async relayMessage( dispatchTx: providers.TransactionReceipt, messageIndex = 0,