Manual Execution
When automatic CCIP message execution fails on the destination chain, you can manually execute the message. This guide covers the complete workflow for manual execution.
When to Manually Execute
Manual execution is needed when:
- Receiver contract reverts - The destination contract throws an error
- Insufficient gas limit - The gas limit set in extraArgs was too low
- Rate limiter blocked - Token transfer exceeded rate limits
- Execution timeout - Automatic execution window expired
Prerequisites
Before manual execution, ensure:
- The message has been committed on the destination chain
- The source chain finality period has passed
- You have a funded wallet on the destination chain
Step-by-Step Workflow
Step 1: Get the Original Request
Retrieve the message from the source chain transaction:
import { EVMChain } from '@chainlink/ccip-sdk'
const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const dest = await EVMChain.fromUrl('https://rpc.fuji.avax.network')
const sourceTxHash = '0x1234...' // Transaction that sent the CCIP message
// getMessagesInTx throws CCIPMessageNotFoundInTxError if no messages found
const requests = await source.getMessagesInTx(sourceTxHash)
const request = requests[0]
console.log('Message ID:', request.message.messageId)
console.log('Sequence Number:', request.message.sequenceNumber)
Step 2: Find the OffRamp Contract
Discover the OffRamp contract on the destination chain:
import { discoverOffRamp } from '@chainlink/ccip-sdk'
const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)
console.log('OffRamp address:', offRamp)
Step 3: Check Execution Status
Verify whether the message needs manual execution:
import { ExecutionState } from '@chainlink/ccip-sdk'
let needsManualExecution = true
for await (const execution of dest.getExecutionReceipts({
offRamp,
messageId: request.message.messageId,
})) {
console.log('Execution state:', ExecutionState[execution.receipt.state])
switch (execution.receipt.state) {
case ExecutionState.Success:
console.log('Message already executed successfully')
needsManualExecution = false
break
case ExecutionState.Failed:
console.log('Previous execution failed')
console.log('Return data:', execution.receipt.returnData)
// Can proceed with manual execution
break
case ExecutionState.InProgress:
console.log('Execution in progress')
// Wait and check again
break
}
}
if (!needsManualExecution) {
process.exit(0)
}
Step 4: Get the Verifications
Verify the message has been committed:
const verifications = await dest.getVerifications({
commitStore: offRamp,
request,
})
if (!verifications) {
console.log('Message not yet committed')
console.log('Wait for the DON to commit the merkle root')
process.exit(1)
}
console.log('Commit found!')
console.log('Merkle root:', verifications.report.merkleRoot)
console.log('Min sequence:', verifications.report.minSeqNr)
console.log('Max sequence:', verifications.report.maxSeqNr)
console.log('Commit tx:', verifications.log.transactionHash)
Step 5: Fetch Execution Inputs and Calculate Proof
Get execution input and calculate proofs:
const input = await source.getExecutionInput({ request, verifications })
console.log('Calculated proof:', input.merkleRoot)
console.log('Offchain tokenData:', input.offchainTokenData)
Step 6: Execute the Report
Submit the manual execution transaction:
const execution = await dest.execute({
offRamp,
input,
wallet: destWallet,
})
console.log('Manual execution submitted:', execution.log.transactionHash)
console.log('Execution confirmed in block:', execution.log.blockNumber)
Complete Example
import { ethers } from 'ethers'
import {
EVMChain,
discoverOffRamp,
ExecutionState,
} from '@chainlink/ccip-sdk'
async function manuallyExecuteMessage(
sourceRpc: string,
destRpc: string,
sourceTxHash: string,
wallet: ethers.Signer
) {
// Connect to chains
const source = await EVMChain.fromUrl(sourceRpc)
const dest = await EVMChain.fromUrl(destRpc)
// Step 1: Get the request (throws CCIPMessageNotFoundInTxError if not found)
const requests = await source.getMessagesInTx(sourceTxHash)
const request = requests[0]
console.log('Processing message:', request.message.messageId)
// Step 2: Find OffRamp
const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)
// Step 3: Check if already executed
for await (const execution of dest.getExecutionReceipts({
offRamp,
messageId: request.message.messageId,
})) {
if (execution.receipt.state === ExecutionState.Success) {
console.log('Already executed')
return { status: 'already_executed' }
}
}
// Step 4: Get commit
const verifications = await dest.getVerifications({ offRamp, request })
if (!verifications) {
console.log('Not yet committed')
return { status: 'pending_commit' }
}
// Step 5: Get messages in batch
const input = await source.getExecutionInput({ request, verifications })
// Step 6: Execute
const execution = await dest.execute({
offRamp,
input,
wallet,
})
console.log('Manual execution tx:', execution.log.transactionHash)
return { status: 'executed', txHash: execution.log.transactionHash }
}
// Usage
const destProvider = new ethers.JsonRpcProvider('https://rpc.fuji.avax.network')
const destWallet = new ethers.Wallet(process.env.DEST_PRIVATE_KEY!, destProvider)
await manuallyExecuteMessage(
'https://rpc.sepolia.org',
'https://rpc.fuji.avax.network',
'0xSourceTxHash...',
destWallet
)
Handling Token Transfers
For messages with token transfers, you may need offchain token data:
USDC (CCTP) Transfers
For USDC transfers using CCTP, you need to fetch the Circle attestation. The SDK handles this automatically:
// Fetch offchain token data (handles USDC attestations automatically)
const input = await source.getExecutionInput({ request, verifications })
console.log('offchainTokenData:', input.offchainTokenData)
const execution = await dest.execute({
offRamp,
input,
wallet: destWallet,
})
Standard Token Transfers
For non-CCTP tokens, offchainTokenData is typically empty:
const execution = await dest.execute({
offRamp,
execReport: {
...proof,
message: request.message,
offchainTokenData: [], // Empty for standard tokens
},
wallet: destWallet,
})
Troubleshooting
Execution Reverts
If manual execution reverts, check:
- Gas limit - Increase gas for the destination execution
- Receiver contract - Ensure the receiver can handle the message
- Token allowances - Verify token pool has sufficient liquidity
// Increase gas limit for execution
const execution = await dest.execute({
offRamp,
input,
wallet: destWallet,
gasLimit: 500000, // Override gas limit
})
Message Not Found
If the message isn't found on the source chain:
import { networkInfo } from '@chainlink/ccip-sdk'
// Verify you're on the correct source chain
const sourceNetwork = networkInfo(source.network.chainSelector)
console.log('Source chain:', sourceNetwork.name)
// Check if the transaction hash is correct
const tx = await source.provider.getTransaction(sourceTxHash)
if (!tx) {
console.log('Transaction not found - verify the hash and chain')
}
Using the CLI
The CLI provides a simpler interface for manual execution:
# Execute a stuck message
ccip-cli manual-exec 0xSourceTxHash \
--source ethereum-testnet-sepolia \
--dest avalanche-testnet-fuji \
--wallet $PRIVATE_KEY
See CLI Manual Exec for more options.
Related
- Error Handling - Error types and recovery
- Tracking Messages - Monitor message status
- CLI Manual Exec - Command-line manual execution