Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions ccip-cli/src/commands/manual-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
estimateReceiveExecution,
isSupportedTxHash,
} from '@chainlink/ccip-sdk/src/index.ts'
import { isHexString } from 'ethers'
import { hexlify, isHexString } from 'ethers'
import type { Argv } from 'yargs'

import type { GlobalOpts } from '../index.ts'
Expand All @@ -44,7 +44,7 @@ import {
selectRequest,
withDateTimestamp,
} from './utils.ts'
import { fetchChainsFromRpcs, loadChainWallet } from '../providers/index.ts'
import { fetchChainsFromRpcs, loadChainWallet, resolveIndexer } from '../providers/index.ts'

// const MAX_QUEUE = 1000
// const MAX_EXECS_IN_BATCH = 1
Expand Down Expand Up @@ -118,6 +118,11 @@ export const builder = (yargs: Argv) =>
string: true,
example: '--receiver-object-ids 0xabc... 0xdef...',
},
receiver: {
type: 'string',
describe:
'Canton destination: CCIPReceiver contract ID, party ID (hint::1220…), or keccak256(party) from the message receiver field. Defaults to the message receiver when executing on Canton.',
},
})

/**
Expand Down Expand Up @@ -205,8 +210,10 @@ async function manualExec(
let inputs
if (source) {
offRamp ??= await discoverOffRamp(source, dest, request.lane.onRamp, source)
const indexer = resolveIndexer(argv, dest, logger, source)
const verifications = await dest.getVerifications({
...argv,
indexer,
offRamp,
request,
})
Expand Down Expand Up @@ -277,10 +284,16 @@ async function manualExec(
'on network',
dest.network.name,
)
const messageReceiver =
typeof request.message.receiver === 'string'
? request.message.receiver
: hexlify(request.message.receiver)

const receipt = await dest.execute({
...argv,
wallet,
...(inputs ?? { messageId: request.message.messageId }),
receiver: argv.receiver ?? messageReceiver,
})

switch (argv.format) {
Expand Down
44 changes: 30 additions & 14 deletions ccip-cli/src/commands/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import type { GlobalOpts } from '../index.ts'
import { showRequests } from './show.ts'
import { type Ctx, Format } from './types.ts'
import { getCtx, logParsedError, parseTokenAmounts } from './utils.ts'
import { fetchChainsFromRpcs, loadChainWallet } from '../providers/index.ts'
import { fetchChainsFromRpcs, loadChainWallet, resolveRouter } from '../providers/index.ts'

export const command = 'send'
export const describe = 'Send a CCIP message from source to destination chain'
Expand All @@ -68,8 +68,8 @@ export const builder = (yargs: Argv) =>
.option('router', {
alias: 'r',
type: 'string',
demandOption: true,
describe: 'Router contract address on source',
describe:
'Router contract address on EVM source, or CCIPSender instance id on Canton source (defaults to canton-config senderInstanceId)',
})
.options({
receiver: {
Expand Down Expand Up @@ -227,9 +227,19 @@ async function sendMessage(
const { output, logger } = ctx
const sourceNetwork = networkInfo(argv.source)
const destNetwork = networkInfo(argv.dest)
const router = resolveRouter(argv, sourceNetwork, logger)
if (!router) {
throw new CCIPArgumentInvalidError(
'router',
sourceNetwork.family === ChainFamily.Canton
? 'required on Canton source: pass -r or set senderInstanceId in canton-config'
: 'required: pass -r with the source router contract address',
)
}

const getChain = fetchChainsFromRpcs(ctx, argv)
const source = await getChain(sourceNetwork.name)
decodeAddress(argv.router, sourceNetwork.family)
decodeAddress(router, sourceNetwork.family)

let data: BytesLike | undefined
if (argv.data) {
Expand Down Expand Up @@ -319,7 +329,7 @@ async function sendMessage(
const estimated = await estimateReceiveExecution({
source,
dest,
routerOrRamp: argv.router,
routerOrRamp: router,
message: {
sender: walletAddress,
receiver,
Expand Down Expand Up @@ -378,7 +388,7 @@ async function sendMessage(
feeToken = (source.constructor as ChainStatic).getAddress(argv.feeToken)
feeTokenInfo = await source.getTokenInfo(feeToken)
} catch (_) {
const feeTokens = await source.getFeeTokens(argv.router)
const feeTokens = await source.getFeeTokens(router)
logger.debug('supported feeTokens:', feeTokens)
for (const [token, info] of Object.entries(feeTokens)) {
if (info.symbol === 'UNKNOWN' || info.symbol !== argv.feeToken) continue
Expand All @@ -389,7 +399,7 @@ async function sendMessage(
if (!feeTokenInfo) throw new CCIPTokenNotFoundError(argv.feeToken)
}
} else {
const nativeToken = await source.getNativeTokenForRouter(argv.router)
const nativeToken = await source.getNativeTokenForRouter(router)
feeTokenInfo = await source.getTokenInfo(nativeToken)
}

Expand All @@ -404,6 +414,7 @@ async function sendMessage(
// calculate fee
const fee = await source.getFee({
...argv,
router,
destChainSelector: destNetwork.chainSelector,
message,
})
Expand Down Expand Up @@ -460,6 +471,7 @@ async function sendMessage(

const request = await source.sendMessage({
...argv,
router,
destChainSelector: destNetwork.chainSelector,
message: { ...message, fee },
wallet,
Expand All @@ -474,11 +486,15 @@ async function sendMessage(
', messageId =>',
request.message.messageId,
)
await showRequests(ctx, {
...argv,
txHashOrId: request.tx.hash,
'tx-hash-or-id': request.tx.hash,
'log-index': undefined,
logIndex: undefined,
})
await showRequests(
ctx,
{
...argv,
txHashOrId: request.tx.hash,
'tx-hash-or-id': request.tx.hash,
'log-index': undefined,
logIndex: undefined,
},
{ request },
)
}
87 changes: 57 additions & 30 deletions ccip-cli/src/commands/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
*/

import {
type CCIPRequest,
type Chain,
type ChainGetter,
type ChainTransaction,
CCIPAPIClient,
CCIPExecTxRevertedError,
CCIPMessageIdNotFoundError,
Expand All @@ -46,7 +49,7 @@ import {
selectRequest,
withDateTimestamp,
} from './utils.ts'
import { fetchChainsFromRpcs } from '../providers/index.ts'
import { fetchChainsFromRpcs, resolveIndexer } from '../providers/index.ts'

export const command = ['show <tx-hash-or-id>', '* <tx-hash-or-id>']
export const describe = 'Show details of a CCIP request'
Expand Down Expand Up @@ -92,8 +95,13 @@ export async function handler(argv: Awaited<ReturnType<typeof builder>['argv']>

/**
* Show details of a request.
* When `opts.request` is set (e.g. after `send`), skip re-fetching the source tx from RPC.
*/
export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]) {
export async function showRequests(
ctx: Ctx,
argv: Parameters<typeof handler>[0],
opts?: { request?: CCIPRequest },
) {
const { output, logger } = ctx

// In JSON mode, accumulate all output into a single envelope so JSON.parse(stdout) works.
Expand All @@ -116,41 +124,58 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
}
}

const [getChain, tx$] = fetchChainsFromRpcs(ctx, argv, argv.txHashOrId)
let getChain: ChainGetter
let tx$: Promise<[Chain, ChainTransaction]> | undefined
if (opts?.request) {
getChain = fetchChainsFromRpcs(ctx, argv)
} else {
const chainsResult = fetchChainsFromRpcs(ctx, argv, argv.txHashOrId)
getChain = chainsResult[0]
tx$ = chainsResult[1]
}

let source: Chain | undefined, offRamp
// Track if we displayed all messages in non-interactive multi-message path
let displayedAllMessages = false as boolean

let request$ = (async () => {
const [source_, tx] = await tx$
source = source_
const messages = await source_.getMessagesInTx(tx)
let request$ = opts?.request
? (async () => {
try {
source = await getChain(opts.request!.lane.sourceChainSelector)
} catch (err) {
logger.debug('Failed to resolve source chain for prebuilt request:', err)
}
return opts.request!
})()
: (async () => {
const [source_, tx] = await tx$!
source = source_
const messages = await source_.getMessagesInTx(tx)

// Non-interactive multi-message path: display all messages and signal early return
if (argv.interactive === false && argv.logIndex == null && messages.length > 1) {
switch (argv.format) {
case Format.log:
for (const req of messages) {
output.write(`message ${req.log.index} =`, withDateTimestamp(req))
}
break
case Format.pretty:
for (const req of messages) {
await prettyRequest.call(ctx, req, source)
// Non-interactive multi-message path: display all messages and signal early return
if (argv.interactive === false && argv.logIndex == null && messages.length > 1) {
switch (argv.format) {
case Format.log:
for (const req of messages) {
output.write(`message ${req.log.index} =`, withDateTimestamp(req))
}
break
case Format.pretty:
for (const req of messages) {
await prettyRequest.call(ctx, req, source)
}
break
case Format.json:
output.write(JSON.stringify({ requests: messages }, bigIntReplacer, 2))
break
}
break
case Format.json:
output.write(JSON.stringify({ requests: messages }, bigIntReplacer, 2))
break
}
logger.info('Use --log-index N for full details on a specific message.')
displayedAllMessages = true
return messages[0]! // return first to satisfy type; caller checks displayedAllMessages
}
logger.info('Use --log-index N for full details on a specific message.')
displayedAllMessages = true
return messages[0]! // return first to satisfy type; caller checks displayedAllMessages
}

return selectRequest(messages, 'to know more', argv)
})()
return selectRequest(messages, 'to know more', argv)
})()

if (argv.api !== false && isHexString(argv.txHashOrId, 32)) {
const apiClient = CCIPAPIClient.fromUrl(
Expand Down Expand Up @@ -258,7 +283,7 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
output.write('Commit (dest):')
})()

let dest: Chain | undefined
let dest!: Chain
try {
dest = await getChain(request.lane.destChainSelector)
} catch (err) {
Expand Down Expand Up @@ -293,10 +318,12 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
cancelWaitVerifications = ac.abort.bind(ac)
}
verifications$ = (async () => {
const indexer = resolveIndexer(argv, dest, logger, source)
const verifications = await dest.getVerifications({
offRamp,
request,
...argv,
indexer,
watch,
})
cancelWaitFinalized?.()
Expand Down
3 changes: 3 additions & 0 deletions ccip-cli/src/commands/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ async function formatToken(
| { sourceTokenAddress?: string; sourcePoolAddress: string }
),
): Promise<string> {
if ('token' in ta && !ta.token) {
return ta.amount === 0n ? '0 (paid on-ledger)' : `${ta.amount} (on-ledger)`
}
if (!source) return `${ta.amount} ${'sourcePoolAddress' in ta ? ta.sourcePoolAddress : ta.token}`
let token
if ('token' in ta) token = ta.token
Expand Down
2 changes: 1 addition & 1 deletion ccip-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Error.stackTraceLimit = 50 // show more stack frames for better debugging

// generate:nofail
// `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'`
const VERSION = '1.9.2-ae443e7'
const VERSION = '1.9.2-9e31827'
// generate:end

const require = createRequire(import.meta.url)
Expand Down
42 changes: 42 additions & 0 deletions ccip-cli/src/providers/canton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,52 @@ export function loadCantonConfig(
}
}

if (parsed['chainId'] != null) {
if (typeof parsed['chainId'] !== 'string' || !parsed['chainId'].length) {
throw new Error('Canton config: "chainId" must be a non-empty string if provided')
}
}

logger?.debug('Loaded Canton config from', configPath, 'for party', parsed['party'])
return parsed as unknown as CantonConfig
}

/**
* CCIP v2 indexer URLs for verification lookups.
* CLI `--indexer` wins when provided; otherwise uses canton-config `indexerUrl`
* only when the lane involves Canton (EVM-only lanes keep default indexer behavior).
* Prefer {@link resolveIndexer} from `./index.ts` in CLI commands.
*/
export function resolveCliIndexer(
cliIndexer: readonly string[] | undefined,
cantonConfig: Partial<CantonConfig> | undefined,
laneInvolvesCanton: boolean,
): readonly string[] | undefined {
if (cliIndexer?.length) return cliIndexer
if (!laneInvolvesCanton) return undefined
const url = cantonConfig?.indexerUrl?.trim()
return url ? [url] : undefined
}

/**
* Router / sender instance id for `ccip-cli send -r`.
* On Canton source lanes this is the CCIPSender instance id (e.g. `prod-ccipsender`);
* on EVM it must be the router contract address. CLI `-r` wins when set.
* Prefer {@link resolveRouter} from `./index.ts` in CLI commands.
*/
export function resolveCliRouter(
cliRouter: string | undefined,
cantonConfig: Partial<CantonConfig> | undefined,
sourceIsCanton: boolean,
): string | undefined {
if (cliRouter?.trim()) return cliRouter.trim()
if (sourceIsCanton) {
const fromConfig = cantonConfig?.senderInstanceId?.trim()
if (fromConfig) return fromConfig
}
return cliRouter
}

/**
* Resolve a Canton wallet from CLI argv.
*
Expand Down
Loading
Loading