From bc28e7295f47e0c656a16bd7752e2a67bdaa722b Mon Sep 17 00:00:00 2001 From: thinktanktom Date: Sun, 1 Feb 2026 12:11:41 +0530 Subject: [PATCH 1/2] feat: add loop to manual-exec for multiple message execution Enhances manual-exec command to allow users to cycle through and execute multiple failed CCIP messages in a single session. Changes: - Wrapped message selection and execution in while(true) loop - Added exit handling when user selects 'Exit' option - Added error handling to prevent loop crashes on execution failures - Moved message fetching outside loop for efficiency Closes #111 --- CHANGELOG.md | 1 + ccip-cli/src/commands/manual-exec.ts | 203 +++++++++++++++------------ 2 files changed, 114 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36f66d9c..8d0f930f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- CLI: `manualExec` command now loops through multiple messages in single session (#111) - SDK: Fix `sleep()` browser compatibility - use optional chaining for `.unref()` which is Node.js-only ## [0.95.0] - 2026-01-28 - Pre-release diff --git a/ccip-cli/src/commands/manual-exec.ts b/ccip-cli/src/commands/manual-exec.ts index 804b7ed2..bc935b98 100644 --- a/ccip-cli/src/commands/manual-exec.ts +++ b/ccip-cli/src/commands/manual-exec.ts @@ -1,5 +1,7 @@ import { + type CCIPRequest, type ExecutionReport, + CCIPError, bigIntReplacer, calculateManualExecProof, discoverOffRamp, @@ -125,103 +127,124 @@ async function manualExec( // messageId not yet implemented for Solana const [getChain, tx$] = fetchChainsFromRpcs(ctx, argv, argv.txHash) const [source, tx] = await tx$ - const request = await selectRequest(await source.getMessagesInTx(tx), 'to know more', argv) - - switch (argv.format) { - case Format.log: { - const logPrefix = 'log' in request ? `message ${request.log.index} = ` : 'message = ' - logger.log(logPrefix, withDateTimestamp(request)) - break + // Store all messages to use inside loop + const messages = await source.getMessagesInTx(tx) + let request: CCIPRequest + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + while (true) { + try { + //Select a message + request = await selectRequest(messages, 'to know more', argv) + } catch (error) { + // Handle the exit case + if (error instanceof CCIPError && error.message === 'User requested exit') { + logger.info('Exiting manual execution.') + break + } + // If it's some other error, re-throw it + throw error } - case Format.pretty: - await prettyRequest.call(ctx, source, request) - break - case Format.json: - logger.info(JSON.stringify(request, bigIntReplacer, 2)) - break - } + switch (argv.format) { + case Format.log: { + const logPrefix = 'log' in request ? `message ${request.log.index} = ` : 'message = ' + logger.log(logPrefix, withDateTimestamp(request)) + break + } + case Format.pretty: + await prettyRequest.call(ctx, source, request) + break + case Format.json: + logger.info(JSON.stringify(request, bigIntReplacer, 2)) + break + } + // try-catch for execution + try { + const dest = await getChain(request.lane.destChainSelector) + const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp, source) + const commitStore = await dest.getCommitStoreForOffRamp(offRamp) + const commit = await dest.getCommitReport({ ...argv, commitStore, request }) - const dest = await getChain(request.lane.destChainSelector) - const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp, source) - const commitStore = await dest.getCommitStoreForOffRamp(offRamp) - const commit = await dest.getCommitReport({ ...argv, commitStore, request }) + switch (argv.format) { + case Format.log: + logger.log('commit =', commit) + break + case Format.pretty: + logger.info('Commit (dest):') + await prettyCommit.call(ctx, dest, commit, request) + break + case Format.json: + logger.info(JSON.stringify(commit, bigIntReplacer, 2)) + break + } - switch (argv.format) { - case Format.log: - logger.log('commit =', commit) - break - case Format.pretty: - logger.info('Commit (dest):') - await prettyCommit.call(ctx, dest, commit, request) - break - case Format.json: - logger.info(JSON.stringify(commit, bigIntReplacer, 2)) - break - } + const messagesInBatch = await source.getMessagesInBatch(request, commit.report, argv) + const execReportProof = calculateManualExecProof( + messagesInBatch, + request.lane, + request.message.messageId, + commit.report.merkleRoot, + dest, + ) - const messagesInBatch = await source.getMessagesInBatch(request, commit.report, argv) - const execReportProof = calculateManualExecProof( - messagesInBatch, - request.lane, - request.message.messageId, - commit.report.merkleRoot, - dest, - ) + const offchainTokenData = await source.getOffchainTokenData(request) + const execReport: ExecutionReport = { + ...execReportProof, + message: request.message, + offchainTokenData, + } - const offchainTokenData = await source.getOffchainTokenData(request) - const execReport: ExecutionReport = { - ...execReportProof, - message: request.message, - offchainTokenData, - } + if (argv.estimateGasLimit != null) { + let estimated = await estimateReceiveExecution({ + source, + dest, + routerOrRamp: offRamp, + message: request.message, + }) + logger.info('Estimated gasLimit override:', estimated) + estimated += Math.ceil((estimated * argv.estimateGasLimit) / 100) + const origLimit = Number( + 'gasLimit' in request.message ? request.message.gasLimit : request.message.computeUnits, + ) + if (origLimit >= estimated) { + logger.warn( + 'Estimated +', + argv.estimateGasLimit, + '% =', + estimated, + '< original gasLimit =', + origLimit, + '. Leaving unchanged.', + ) + } else { + argv.gasLimit = estimated + } + } - if (argv.estimateGasLimit != null) { - let estimated = await estimateReceiveExecution({ - source, - dest, - routerOrRamp: offRamp, - message: request.message, - }) - logger.info('Estimated gasLimit override:', estimated) - estimated += Math.ceil((estimated * argv.estimateGasLimit) / 100) - const origLimit = Number( - 'gasLimit' in request.message ? request.message.gasLimit : request.message.computeUnits, - ) - if (origLimit >= estimated) { - logger.warn( - 'Estimated +', - argv.estimateGasLimit, - '% =', - estimated, - '< original gasLimit =', - origLimit, - '. Leaving unchanged.', - ) - } else { - argv.gasLimit = estimated - } - } + const [, wallet] = await loadChainWallet(dest, argv) + const receipt = await dest.executeReport({ ...argv, offRamp, execReport, wallet }) - const [, wallet] = await loadChainWallet(dest, argv) - const receipt = await dest.executeReport({ ...argv, offRamp, execReport, wallet }) - - switch (argv.format) { - case Format.log: - logger.log('receipt =', withDateTimestamp(receipt)) - break - case Format.pretty: - logger.info('Receipt (dest):') - prettyReceipt.call( - ctx, - receipt, - request, - receipt.log.tx?.from ?? - (await dest.getTransaction(receipt.log.transactionHash).catch(() => null))?.from, - ) - break - case Format.json: - logger.info(JSON.stringify(receipt, bigIntReplacer, 2)) - break + switch (argv.format) { + case Format.log: + logger.log('receipt =', withDateTimestamp(receipt)) + break + case Format.pretty: + logger.info('Receipt (dest):') + prettyReceipt.call( + ctx, + receipt, + request, + receipt.log.tx?.from ?? + (await dest.getTransaction(receipt.log.transactionHash).catch(() => null))?.from, + ) + break + case Format.json: + logger.info(JSON.stringify(receipt, bigIntReplacer, 2)) + break + } + } catch (error) { + logger.error('Message execution failed:', error) + logger.info('Returning to message selection...\n') + } } } From f1151f5a0835db98005a02f0f95559e21d08fd83 Mon Sep 17 00:00:00 2001 From: thinktanktom Date: Sun, 1 Feb 2026 12:30:26 +0530 Subject: [PATCH 2/2] fix: prevent infinite loop on single message transactions selectRequest auto-returns when only one message exists, which would cause the loop to execute the same message repeatedly. Added check to exit after executing single-message transactions. --- ccip-cli/src/commands/manual-exec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ccip-cli/src/commands/manual-exec.ts b/ccip-cli/src/commands/manual-exec.ts index bc935b98..73fac2ee 100644 --- a/ccip-cli/src/commands/manual-exec.ts +++ b/ccip-cli/src/commands/manual-exec.ts @@ -245,6 +245,11 @@ async function manualExec( logger.error('Message execution failed:', error) logger.info('Returning to message selection...\n') } + // Exit if only one message (prevents infinite loop since selectRequest auto-selects single messages) + if (messages.length === 1) { + logger.info('Single message transaction - exiting after execution.') + break + } } }