Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

- SDK: `getFeeTokens` now supports CCIP v2.0 lanes (via FeeQuoter, same as v1.6)
- SDK: implemented `CCIPAPIClient.getExecutionInput(messageId: string)`
- SDK: `dest.execute` and `dest.generateUnsignedExecute` can now execute directly from a `messageId`, without the need for an explicit `source.getExecutionInput`
- SDK: `Chain` constructors context can receive a string URL for `apiClient`
- CLI: using all the above, `manual-exec` now can receive a `messageId` as positional argument (besides `txHash`), and execute from CCIP-API's `/execution-inputs` without needing a source RPC

## [1.0.0] - 2026-02-26 - Major refactoring stable

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ constructor(network: NetworkInfo, ctx?: ChainContext) {
} else if (apiClient !== undefined) {
this.apiClient = apiClient // Use provided instance
} else {
this.apiClient = CCIPAPIClient.fromUrl(undefined, { logger }) // Default
this.apiClient = CCIPAPIClient.fromUrl(undefined, ctx) // Default
}
}
```
Expand Down
8 changes: 4 additions & 4 deletions ccip-api-ref/docs-cli/lane-latency.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ The latency estimate includes time for:

## Options

| Option | Type | Default | Description |
| ----------- | ------ | ----------------------------- | ------------------- |
| `--api-url` | string | `https://api.ccip.chain.link` | Custom CCIP API URL |
| Option | Type | Default | Description |
| ------- | ------ | ----------------------------- | ------------------- |
| `--api` | string | `https://api.ccip.chain.link` | Custom CCIP API URL |

See [Configuration](/cli/configuration) for global options (`--format`, `--no-api`, etc.).

Expand Down Expand Up @@ -85,7 +85,7 @@ ccip-cli lane-latency ethereum-mainnet arbitrum-mainnet --format json
### Use custom API endpoint

```bash
ccip-cli lane-latency ethereum-mainnet arbitrum-mainnet --api-url https://staging-api.example.com
ccip-cli lane-latency ethereum-mainnet arbitrum-mainnet --api https://staging-api.example.com
```

## Output
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export const laneLatencySchema: CommandSchema<'laneLatency'> = {
options: [
{
type: 'string',
name: 'api-url',
name: 'api',
label: 'API URL',
description: 'Custom CCIP API URL (defaults to api.ccip.chain.link)',
description: 'Custom CCIP API URL (defaults to https://api.ccip.chain.link)',
group: 'output',
placeholder: 'https://api.ccip.chain.link',
},
Expand Down
4 changes: 2 additions & 2 deletions ccip-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ ccip-cli token -n solana-devnet -H EPUjBP3Xf76K1VKsDSc6GupBWE8uykNksCLJgXZn87CB
### `lane-latency`

```sh
ccip-cli lane-latency <source> <dest> [--api-url <url>]
ccip-cli lane-latency <source> <dest> [--api=<url>]
```

Query real-time lane latency between source and destination chains using the CCIP API.
Expand All @@ -355,7 +355,7 @@ Query real-time lane latency between source and destination chains using the CCI

| Option | Description |
|--------|-------------|
| `--api-url` | Custom CCIP API URL (default: api.ccip.chain.link) |
| `--api` | Custom CCIP API URL (default: https://api.ccip.chain.link) |

> **Note:** This command requires CCIP API access and respects the `--no-api` flag.

Expand Down
2 changes: 1 addition & 1 deletion ccip-cli/src/commands/lane-latency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('lane-latency command', () => {
await getLaneLatencyCmd(createCtx(), {
source: '1',
dest: '42161',
apiUrl: 'https://custom.api.example.com/',
api: 'https://custom.api.example.com/',
format: Format.json,
} as Parameters<typeof getLaneLatencyCmd>[1])

Expand Down
8 changes: 2 additions & 6 deletions ccip-cli/src/commands/lane-latency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* ccip-cli lane-latency ethereum-mainnet arbitrum-mainnet
*
* # Use custom API URL
* ccip-cli lane-latency sepolia fuji --api-url https://custom-api.example.com
* ccip-cli lane-latency sepolia fuji --api https://custom-api.example.com
* ```
*
* @packageDocumentation
Expand Down Expand Up @@ -49,10 +49,6 @@ export const builder = (yargs: Argv) =>
demandOption: true,
describe: 'Destination network (chainId, selector, or name). Example: arbitrum-mainnet',
})
.option('api-url', {
type: 'string',
describe: 'Custom CCIP API URL (defaults to api.ccip.chain.link)',
})

/**
* Handler for the lane-latency command.
Expand Down Expand Up @@ -85,7 +81,7 @@ export async function getLaneLatencyCmd(ctx: Ctx, argv: Parameters<typeof handle
const sourceNetwork = networkInfo(argv.source)
const destNetwork = networkInfo(argv.dest)

const apiClient = CCIPAPIClient.fromUrl(argv.apiUrl, { logger })
const apiClient = CCIPAPIClient.fromUrl(argv.api === true ? undefined : argv.api, ctx)

const result = await apiClient.getLaneLatency(
sourceNetwork.chainSelector,
Expand Down
143 changes: 80 additions & 63 deletions ccip-cli/src/commands/manual-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@
*/

import {
type CCIPRequest,
type Chain,
CCIPAPIClient,
CCIPMessageIdNotFoundError,
CCIPTransactionNotFoundError,
bigIntReplacer,
discoverOffRamp,
estimateReceiveExecution,
isSupportedTxHash,
} from '@chainlink/ccip-sdk/src/index.ts'
import { isHexString } from 'ethers'
import type { Argv } from 'yargs'

import type { GlobalOpts } from '../index.ts'
Expand All @@ -34,7 +40,6 @@ import {
logParsedError,
prettyReceipt,
prettyRequest,
prettyVerifications,
selectRequest,
withDateTimestamp,
} from './utils.ts'
Expand All @@ -44,7 +49,7 @@ import { fetchChainsFromRpcs, loadChainWallet } from '../providers/index.ts'
// const MAX_EXECS_IN_BATCH = 1
// const MAX_PENDING_TXS = 25

export const command = ['manualExec <tx-hash>', 'manual-exec <tx-hash>']
export const command = ['manualExec <tx-hash-or-id>', 'manual-exec <tx-hash-or-id>']
export const describe = 'Execute manually pending or failed messages'

/**
Expand All @@ -54,12 +59,12 @@ export const describe = 'Execute manually pending or failed messages'
*/
export const builder = (yargs: Argv) =>
yargs
.positional('tx-hash', {
.positional('tx-hash-or-id', {
type: 'string',
demandOption: true,
describe: 'transaction hash of the request (source) message',
})
.check(({ txHash }) => isSupportedTxHash(txHash))
.check(({ 'tx-hash-or-id': txHashOrId }) => isSupportedTxHash(txHashOrId))
.options({
'log-index': {
type: 'number',
Expand Down Expand Up @@ -106,17 +111,6 @@ export const builder = (yargs: Argv) =>
string: true,
example: '--receiver-object-ids 0xabc... 0xdef...',
},
'sender-queue': {
type: 'boolean',
describe: 'Execute all messages in sender queue, starting with the provided tx',
default: false,
},
'exec-failed': {
type: 'boolean',
describe:
'Whether to re-execute failed messages (instead of just non-executed) in sender queue',
implies: 'sender-queue',
},
})

/**
Expand All @@ -141,10 +135,34 @@ async function manualExec(
argv: Awaited<ReturnType<typeof builder>['argv']> & GlobalOpts,
) {
const { logger } = ctx
// 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)
const [getChain, tx$] = fetchChainsFromRpcs(ctx, argv, argv.txHashOrId)

let source: Chain | undefined, offRamp
let request$: Promise<CCIPRequest> | ReturnType<CCIPAPIClient['getMessageById']> = (async () => {
const [source_, tx] = await tx$
source = source_
return selectRequest(await source_.getMessagesInTx(tx), 'to know more', argv)
})()

let apiClient
if (argv.api !== false && isHexString(argv.txHashOrId, 32)) {
apiClient = CCIPAPIClient.fromUrl(typeof argv.api === 'string' ? argv.api : undefined, ctx)
request$ = Promise.any([request$, apiClient.getMessageById(argv.txHashOrId)])
}

let request
try {
request = await request$
if ('offRampAddress' in request.message) {
offRamp = request.message.offRampAddress
}
} catch (err) {
if (err instanceof AggregateError && err.errors.length === 2) {
if (!(err.errors[0] instanceof CCIPTransactionNotFoundError)) throw err.errors[0] as Error
else if (!(err.errors[1] instanceof CCIPMessageIdNotFoundError)) throw err.errors[1] as Error
}
throw err
}

switch (argv.format) {
case Format.log: {
Expand All @@ -161,57 +179,56 @@ async function manualExec(
}

const dest = await getChain(request.lane.destChainSelector)
const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp, source)
// `--estimate-gas-limit` requires source
if (argv.estimateGasLimit != null && !source)
source = await getChain(request.lane.sourceChainSelector)

const verifications = await dest.getVerifications({ ...argv, offRamp, request })
switch (argv.format) {
case Format.log:
logger.log('commit =', verifications)
break
case Format.pretty:
logger.info('Commit (dest):')
await prettyVerifications.call(ctx, dest, verifications, request)
break
case Format.json:
logger.info(JSON.stringify(verifications, bigIntReplacer, 2))
break
}
let inputs
if (source) {
offRamp ??= await discoverOffRamp(source, dest, request.lane.onRamp, source)
const verifications = await dest.getVerifications({ ...argv, offRamp, request })

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(
'ccipReceiveGasLimit' in request.message
? request.message.ccipReceiveGasLimit
: 'gasLimit' in request.message
? request.message.gasLimit
: request.message.computeUnits,
)
if (origLimit >= estimated) {
logger.warn(
'Estimated +',
argv.estimateGasLimit,
'% =',
estimated,
'< original gasLimit =',
origLimit,
'. Leaving unchanged.',
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(
'ccipReceiveGasLimit' in request.message
? request.message.ccipReceiveGasLimit
: 'gasLimit' in request.message
? request.message.gasLimit
: request.message.computeUnits,
)
} else {
argv.gasLimit = estimated
if (origLimit >= estimated) {
logger.warn(
'Estimated +',
argv.estimateGasLimit,
'% =',
estimated,
'< original gasLimit =',
origLimit,
'. Leaving unchanged.',
)
} else {
argv.gasLimit = estimated
}
}
}

const input = await source.getExecutionInput({ ...argv, request, verifications })
const input = await source.getExecutionInput({ ...argv, request, verifications })
inputs = { input, offRamp }
}

const [, wallet] = await loadChainWallet(dest, argv)
const receipt = await dest.execute({ ...argv, offRamp, input, wallet })
const receipt = await dest.execute({
...argv,
wallet,
...(inputs ?? { messageId: request.message.messageId }),
})

switch (argv.format) {
case Format.log:
Expand Down
16 changes: 10 additions & 6 deletions ccip-cli/src/commands/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,25 +97,27 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
const { logger } = ctx
const [getChain, tx$] = fetchChainsFromRpcs(ctx, argv, argv.txHashOrId)

let source: Chain | undefined
let source: Chain | undefined, offRamp
let request$ = (async () => {
const [source_, tx] = await tx$
source = source_
return selectRequest(await source_.getMessagesInTx(tx), 'to know more', argv)
})()

if (argv.api !== false) {
if (argv.api !== false && isHexString(argv.txHashOrId, 32)) {
const apiClient = CCIPAPIClient.fromUrl(
typeof argv.api === 'string' ? argv.api : undefined,
ctx,
)
if (isHexString(argv.txHashOrId, 32)) {
request$ = Promise.any([request$, apiClient.getMessageById(argv.txHashOrId)])
}
request$ = Promise.any([request$, apiClient.getMessageById(argv.txHashOrId)])
}

let request
try {
request = await request$
if ('offRampAddress' in request.message) {
offRamp = request.message.offRampAddress
}
} catch (err) {
if (err instanceof AggregateError && err.errors.length === 2) {
if (!(err.errors[0] instanceof CCIPTransactionNotFoundError)) throw err.errors[0] as Error
Expand All @@ -124,6 +126,8 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
throw err
}
if (!source) {
// source isn't strictly needed when fetching messageId from API, but it may be useful to print
// more information, e.g. request's token symbols
try {
source = await getChain(request.lane.sourceChainSelector)
} catch (err) {
Expand Down Expand Up @@ -193,7 +197,7 @@ export async function showRequests(ctx: Ctx, argv: Parameters<typeof handler>[0]
})()

const dest = await getChain(request.lane.destChainSelector)
const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp, source)
offRamp ??= await discoverOffRamp(source, dest, request.lane.onRamp, source)

let cancelWaitVerifications: (() => void) | undefined
const verifications$ = (async () => {
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 @@ -11,7 +11,7 @@ import { Format } from './commands/index.ts'
util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests
// generate:nofail
// `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'`
const VERSION = '1.0.0-de8f90f'
const VERSION = '1.0.0-27ff6e8'
// generate:end

const globalOpts = {
Expand Down
7 changes: 1 addition & 6 deletions ccip-cli/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
type ChainTransaction,
type EVMChain,
type TONChain,
CCIPAPIClient,
CCIPChainFamilyUnsupportedError,
CCIPRpcNotFoundError,
CCIPTransactionNotFoundError,
Expand Down Expand Up @@ -106,11 +105,7 @@ export function fetchChainsFromRpcs(
const chain$ = C.fromUrl(url, {
...ctx,
apiClient:
argv.api === false
? null
: typeof argv.api === 'string'
? CCIPAPIClient.fromUrl(argv.api)
: undefined,
argv.api === false ? null : typeof argv.api === 'string' ? argv.api : undefined,
})
chains$.push(chain$)

Expand Down
Loading
Loading