diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2db254..66bc5a5a 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: Add `chains` command to list and lookup CCIP chain configurations (uses docs config API) - SDK: **Breaking**: Reduce bundle size by eliminating cross-chain imports - Move `DEFAULT_GAS_LIMIT` from `evm/const.ts` to `shared/constants.ts` - Move BCS codecs and encoding utils to `shared/bcs-codecs.ts` (shared by Aptos/Sui) diff --git a/ccip-api-ref/docs-cli/chains.mdx b/ccip-api-ref/docs-cli/chains.mdx new file mode 100644 index 00000000..754c663f --- /dev/null +++ b/ccip-api-ref/docs-cli/chains.mdx @@ -0,0 +1,249 @@ +--- +id: chains +title: 'chains' +description: 'List and lookup CCIP chain configurations with chain identifiers, selectors, and family information.' +sidebar_label: chains +sidebar_position: 1 +custom_edit_url: null +--- + +# chains + + + +List and lookup CCIP-supported chains with their identifiers, chain selectors, and chain family. + +## Synopsis + +```bash +ccip-cli chains [identifier] [options] +``` + +## Description + +The `chains` command provides chain identifier mappings for CCIP-supported networks. Each chain has: + +- **Name**: Internal chain identifier (e.g., `ethereum-mainnet`) +- **Chain ID**: Native chain identifier (e.g., `1` for Ethereum) +- **Chain Selector**: Unique CCIP identifier used in cross-chain messaging + +Chain data is sourced from the [CCIP Directory](https://docs.chain.link/ccip/directory). Use this command to find the correct identifiers before using other CLI commands like `send`, `show`, or `laneLatency`. + +## Arguments + +| Argument | Type | Required | Description | +| -------------- | ------ | -------- | -------------------------------------------------------- | +| `[identifier]` | string | No | Chain name, chainId, or selector for single chain lookup | + +When an identifier is provided, displays detailed information for that chain. Without an identifier, lists all chains. + +## Options + +### Filter Options + +| Option | Alias | Type | Description | +| ----------- | ----- | ------- | ----------------------------------------------------------- | +| `--family` | - | string | Filter by chain family: `EVM`, `SVM`, `APTOS`, `SUI`, `TON` | +| `--mainnet` | - | boolean | Show only mainnet chains | +| `--testnet` | - | boolean | Show only testnet chains | +| `--search` | `-s` | string | Search chains by name or display name | + +### Output Options + +| Option | Alias | Type | Description | +| --------------- | ----- | ------- | -------------------------------------------- | +| `--count` | - | boolean | Output chain count only | +| `--field` | - | string | Extract a single field value (for scripting) | +| `--interactive` | `-i` | boolean | Interactive search with type-ahead filtering | + +See [Configuration](/cli/configuration) for global options (`--format`, etc.). + +## Command Builder + +Build your `chains` command interactively: + + + +## Examples + +### List all supported chains + +```bash +ccip-cli chains +``` + +### Lookup a specific chain by name + +```bash +ccip-cli chains ethereum-mainnet +``` + +Output: + +``` +Name: ethereum-mainnet +DisplayName: Ethereum +Selector: 5009297550715157269 +ChainId: 1 +Family: EVM +Testnet: false +Supported: Yes +``` + +### Lookup by chain selector + +```bash +ccip-cli chains 5009297550715157269 +``` + +### Lookup by chain ID + +```bash +ccip-cli chains 1 +``` + +### Filter by chain family + +```bash +# List all EVM chains +ccip-cli chains --family EVM + +# List all Solana chains +ccip-cli chains --family SVM +``` + +### Filter by network type + +```bash +# Mainnets only +ccip-cli chains --mainnet + +# Testnets only +ccip-cli chains --testnet +``` + +### Search for chains + +```bash +# Find chains with "arbitrum" in the name +ccip-cli chains -s arbitrum + +# Find chains by partial chain ID +ccip-cli chains -s 42161 +``` + +### Combine filters + +```bash +# EVM mainnets only +ccip-cli chains --family EVM --mainnet +``` + +### Count chains + +```bash +# Total supported chains +ccip-cli chains --count + +# Count EVM testnets +ccip-cli chains --family EVM --testnet --count +``` + +### Extract specific field for scripting + +```bash +# Get chain selector for use in scripts +SELECTOR=$(ccip-cli chains ethereum-mainnet --field chainSelector) +echo $SELECTOR +# Output: 5009297550715157269 +``` + +### JSON output + +```bash +ccip-cli chains ethereum-mainnet --format json +``` + +Output: + +```json +{ + "chainId": 1, + "chainSelector": "5009297550715157269", + "name": "ethereum-mainnet", + "family": "EVM", + "networkType": "MAINNET", + "displayName": "Ethereum", + "environment": "mainnet" +} +``` + +### Interactive search mode + +```bash +ccip-cli chains -i +``` + +Provides type-ahead filtering to find chains by name. + +## Output Fields + +### List Output + +| Column | Description | +| ----------- | ---------------------------------------------------- | +| DisplayName | Human-readable chain name (e.g., `Ethereum`) | +| Name | Internal chain identifier (e.g., `ethereum-mainnet`) | +| Selector | CCIP chain selector (unique identifier) | +| Family | Chain family (EVM, SVM, APTOS, SUI, TON) | +| Network | Environment (mainnet or testnet) | +| Supported | Whether chain is supported by CCIP | + +### Single Chain Output + +| Field | Description | +| ----------- | ---------------------------------------------------- | +| Name | Internal chain identifier (e.g., `ethereum-mainnet`) | +| DisplayName | Human-readable chain name | +| Selector | CCIP chain selector (use in `--dest`, `--source`) | +| ChainId | Native chain ID | +| Family | Chain family | +| Testnet | Whether chain is a testnet | +| Supported | Whether chain is supported by CCIP | + +## Chain Families + +| Family | Description | +| ------- | ------------------------------- | +| `EVM` | Ethereum Virtual Machine chains | +| `SVM` | Solana Virtual Machine (Solana) | +| `APTOS` | Aptos blockchain | +| `SUI` | Sui blockchain | +| `TON` | TON blockchain | + +## Data Source + +Chain data is fetched from the CCIP docs configuration API, which reflects the current supported chains in the [CCIP Directory](https://docs.chain.link/ccip/directory). The API includes search and filtering capabilities. + +Data is cached for 5 minutes to reduce API calls. The SDK's `networkInfo()` provides canonical chain data (name, chainId, selector, family), while the API provides display names. + +## See Also + +- [CCIP Directory](https://docs.chain.link/ccip/directory) - Official supported chains reference +- [send](/cli/send) - Send a cross-chain message (use chain name or selector) +- [show](/cli/show) - Display details of a CCIP request +- [laneLatency](/cli/lane-latency) - Query lane latency between chains +- [Configuration](/cli/configuration) - RPC and output format options + +## Exit Codes + +| Code | Meaning | +| ---- | -------------------------------------- | +| `0` | Success - chain(s) found and displayed | +| `1` | Error (chain not found, API failure) | + +Use in scripts: + +```bash +ccip-cli chains ethereum-mainnet --field chainSelector && echo "Found" || echo "Not found" +``` diff --git a/ccip-api-ref/docs-cli/index.mdx b/ccip-api-ref/docs-cli/index.mdx index cae6c13d..2e922ea8 100644 --- a/ccip-api-ref/docs-cli/index.mdx +++ b/ccip-api-ref/docs-cli/index.mdx @@ -138,6 +138,7 @@ ccip-cli show | Task | Command | Documentation | | ----------------------- | ---------------------------------------------------- | ------------------------------------------- | +| List/lookup chains | `ccip-cli chains [identifier]` | [chains](/cli/chains) | | Track a message | `ccip-cli show ` | [show](/cli/show) | | Send a message | `ccip-cli send -s -d -r ` | [send](/cli/send) | | Execute pending message | `ccip-cli manualExec ` | [manualExec](/cli/manual-exec) | diff --git a/ccip-api-ref/sidebars-cli.ts b/ccip-api-ref/sidebars-cli.ts index df5efb15..e6d302c8 100644 --- a/ccip-api-ref/sidebars-cli.ts +++ b/ccip-api-ref/sidebars-cli.ts @@ -48,6 +48,11 @@ const sidebars: SidebarsConfig = { label: 'Commands', collapsed: false, items: [ + { + type: 'doc', + id: 'chains', + label: 'chains', + }, { type: 'doc', id: 'show', diff --git a/ccip-cli/README.md b/ccip-cli/README.md index 902b0640..1fde0afc 100644 --- a/ccip-cli/README.md +++ b/ccip-cli/README.md @@ -116,6 +116,44 @@ don't support large ranges) - `CCIP_VERBOSE=true` → same as `--verbose` - `CCIP_FORMAT=json` → same as `--format=json` +### `chains` + +```sh +ccip-cli chains [identifier] [--family EVM|SVM|APTOS|SUI|TON] [--mainnet|--testnet] [-s search] +``` + +List and lookup CCIP chain configurations. Use this to find chain names, chain IDs, and chain selectors. + +| Option | Alias | Description | +|--------|-------|-------------| +| `identifier` | | Chain name, chainId, or selector for single lookup | +| `--family` | | Filter by chain family | +| `--mainnet` | | Show only mainnets | +| `--testnet` | | Show only testnets | +| `--search` | `-s` | Search chains by name | +| `--count` | | Output count only | +| `--field` | | Extract single field value | +| `--interactive` | `-i` | Interactive type-ahead search | + +#### Examples + +```sh +# List all chains +ccip-cli chains + +# Lookup specific chain +ccip-cli chains ethereum-mainnet + +# Filter EVM mainnets +ccip-cli chains --family EVM --mainnet + +# Search for chains +ccip-cli chains -s arbitrum + +# Get chain selector for scripting +ccip-cli chains ethereum-mainnet --field chainSelector +``` + ### `send` ```sh diff --git a/ccip-cli/src/commands/chains.ts b/ccip-cli/src/commands/chains.ts new file mode 100644 index 00000000..cb4aa2d9 --- /dev/null +++ b/ccip-cli/src/commands/chains.ts @@ -0,0 +1,241 @@ +/** + * CCIP Chain Discovery Command + * + * Lists and looks up CCIP chain configurations with support for: + * - Single chain lookup by name, chainId, or selector + * - Filtering by chain family, mainnet/testnet + * - Search for chains by name + * - Interactive search with type-ahead filtering + * - JSON output for scripting + * - Field extraction for specific values + */ + +import { type Logger, ChainFamily, networkInfo } from '@chainlink/ccip-sdk/src/index.ts' +import { search } from '@inquirer/prompts' +import type { Argv } from 'yargs' + +import type { GlobalOpts } from '../index.ts' +import { type Ctx, Format } from './types.ts' +import { getCtx, logParsedError } from './utils.ts' +import { + type ChainInfo, + type Environment, + fetchAllChains, + getAllChainsFlat, + searchChainsAPI, +} from '../services/docs-config-api.ts' + +export const command = 'chains [identifier]' +export const describe = 'List and lookup CCIP chain configuration' + +/** + * Yargs builder for the chains command. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv) => + yargs + .positional('identifier', { + type: 'string', + describe: 'Chain name, chainId, or selector to lookup', + }) + .options({ + family: { + type: 'string', + choices: [ + ChainFamily.EVM, + ChainFamily.Solana, + ChainFamily.Aptos, + ChainFamily.Sui, + ChainFamily.TON, + ] as const, + describe: 'Filter by chain family (EVM, SVM, APTOS, SUI, TON)', + }, + mainnet: { type: 'boolean', describe: 'Show only mainnets' }, + testnet: { type: 'boolean', describe: 'Show only testnets' }, + search: { alias: 's', type: 'string', describe: 'Search chains by name' }, + interactive: { + alias: 'i', + type: 'boolean', + describe: 'Interactive search with type-ahead filtering', + }, + count: { type: 'boolean', describe: 'Show count summary only' }, + field: { type: 'string', describe: 'Output only a specific field value' }, + }) + +/** + * Handler for the chains command. + * @param argv - Command line arguments. + */ +export async function handler(argv: Awaited['argv']> & GlobalOpts) { + const [ctx, destroy] = getCtx(argv) + return listChains(ctx, argv) + .catch((err) => { + process.exitCode = 1 + if (!logParsedError.call(ctx, err)) ctx.logger.error(err) + }) + .finally(destroy) +} + +/** + * Helper for BigInt serialization in JSON. + */ +function replacer(_key: string, value: unknown) { + return typeof value === 'bigint' ? value.toString() : value +} + +async function listChains( + ctx: Ctx, + argv: Awaited['argv']> & GlobalOpts, +) { + const { logger } = ctx + + // Determine environment from flags (passthrough to API) + const environment: Environment | undefined = argv.mainnet + ? 'mainnet' + : argv.testnet + ? 'testnet' + : undefined + + // 1. Fetch chains from API (passthrough environment and search to API) + const searchTerm = argv.identifier ?? argv.search + let chains: ChainInfo[] + try { + const responses = searchTerm + ? await searchChainsAPI(searchTerm, environment, logger) + : await fetchAllChains(environment, logger) + chains = getAllChainsFlat(responses) + } catch (err) { + logger.error('Failed to fetch chains from API after retries:', (err as Error).message) + process.exitCode = 1 + return + } + + if (chains.length === 0) { + logger.error(searchTerm ? `No chains found for: ${searchTerm}` : 'No chains found') + process.exitCode = 1 + return + } + + // 2. Apply family filter using SDK's networkInfo for consistent family values + if (argv.family) { + chains = chains.filter((chain) => { + try { + const info = networkInfo(BigInt(chain.chainSelector)) + return info.family === argv.family + } catch { + // Chain not in SDK - exclude from family filter results + return false + } + }) + } + + // 3. Output + if (argv.count) { + logger.log(chains.length) + return + } + + if (argv.field) { + for (const chain of chains) { + logger.log(String(chain[argv.field as keyof ChainInfo])) + } + return + } + + if (argv.format === Format.json) { + logger.log(JSON.stringify(chains, replacer, 2)) + return + } + + // 6. Interactive search mode + if (argv.interactive) { + const selected = await interactiveSearch(chains, environment, logger) + if (selected) { + logger.log(`\nName: ${selected.name}`) + logger.log(`DisplayName: ${selected.displayName}`) + logger.log(`Selector: ${selected.chainSelector}`) + logger.log(`ChainId: ${selected.chainId}`) + logger.log(`Family: ${selected.family}`) + logger.log(`Environment: ${selected.environment}`) + logger.log(`Supported: ${selected.supported ? 'Yes' : 'No'}`) + } + return + } + + // Table output + const displayNameWidth = Math.min( + 25, + Math.max(12, ...chains.map((n) => n.displayName.length)) + 2, + ) + const nameWidth = Math.min(35, Math.max(20, ...chains.map((n) => n.name.length)) + 2) + const selectorWidth = 22 + const familyWidth = 7 + const envWidth = 9 + const supportedWidth = 10 + + logger.log( + 'DisplayName'.padEnd(displayNameWidth) + + 'Name'.padEnd(nameWidth) + + 'Selector'.padEnd(selectorWidth) + + 'Family'.padEnd(familyWidth) + + 'Network'.padEnd(envWidth) + + 'Supported', + ) + logger.log( + '-'.repeat( + displayNameWidth + nameWidth + selectorWidth + familyWidth + envWidth + supportedWidth, + ), + ) + + for (const n of chains) { + logger.log( + n.displayName.padEnd(displayNameWidth) + + n.name.padEnd(nameWidth) + + n.chainSelector.padEnd(selectorWidth) + + n.family.padEnd(familyWidth) + + n.environment.padEnd(envWidth) + + (n.supported ? 'Yes' : 'No'), + ) + } + logger.log(`\nTotal: ${chains.length} chains`) +} + +/** + * Interactive search with type-ahead filtering using inquirer/prompts. + * Uses API search on each keystroke (passthrough to API). + */ +async function interactiveSearch( + initialChains: ChainInfo[], + environment?: Environment, + logger?: Logger, +): Promise { + if (initialChains.length === 0) { + return undefined + } + + return search({ + message: 'Search and select a chain:', + pageSize: 15, + source: async (term) => { + // Use API search when term provided, otherwise use initial chains (cached) + const chains = term + ? getAllChainsFlat(await searchChainsAPI(term, environment, logger)) + : initialChains + + if (chains.length === 0) { + return [] + } + + const nameWidth = Math.min(30, Math.max(15, ...chains.map((c) => c.displayName.length))) + const familyWidth = 8 + + return chains.map((chain, i) => ({ + name: `${chain.displayName.padEnd(nameWidth)} ${chain.family.padEnd(familyWidth)}`, + value: chain, + short: chain.displayName, + description: `${i + 1}/${chains.length} | selector: ${chain.chainSelector}`, + })) + }, + }) +} diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 60df92b0..3311c528 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -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 = '0.96.0-9917492' +const VERSION = '0.96.0-bee96cb' // generate:end const globalOpts = { diff --git a/ccip-cli/src/services/docs-config-api.ts b/ccip-cli/src/services/docs-config-api.ts new file mode 100644 index 00000000..569ce54c --- /dev/null +++ b/ccip-cli/src/services/docs-config-api.ts @@ -0,0 +1,191 @@ +/** + * CCIP Docs Config API Client + * Fetches chain configuration from https://docs.chain.link/api/ccip/v1/chains + * Note: This is NOT the official CCIP API - used for display names only + */ + +import { + type Logger, + CCIPHttpError, + DEFAULT_API_RETRY_CONFIG, + NetworkType, + networkInfo, + withRetry, +} from '@chainlink/ccip-sdk/src/index.ts' + +/** Chain details returned by the CCIP docs config API. */ +export interface ChainDetailsAPI { + chainId: number | string + displayName: string + selector: string + internalId: string + feeTokens: string[] + router: string + chainFamily: string + supported: boolean +} + +/** Response structure from the CCIP docs config API. */ +export interface ChainsAPIResponse { + metadata: { + environment: 'mainnet' | 'testnet' + timestamp: string + validChainCount: number + } + data: Record> // family -> chains +} + +/** Environment type for CCIP chains. */ +export type Environment = 'mainnet' | 'testnet' + +/** Chain info from API (passthrough, no SDK processing). */ +export type ChainInfo = { + name: string // internalId from API + chainId: number | string + chainSelector: string + family: string // chainFamily from API + displayName: string + environment: Environment + supported: boolean +} + +// Constants +const API_BASE = 'https://docs.chain.link/api/ccip/v1/chains' +const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes + +// Cache structure +const cache = new Map() + +/** + * Fetch chains from the CCIP docs config API for a specific environment. + * Uses exponential backoff for transient errors. + * @param environment - The environment to fetch chains for ('mainnet' or 'testnet') + * @param logger - Optional logger for retry attempts + * @returns Promise resolving to the API response + */ +export async function fetchChains( + environment: Environment, + logger?: Logger, +): Promise { + // Check cache first + const cached = cache.get(environment) + if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) { + return cached.data + } + + const url = `${API_BASE}?environment=${environment}&outputKey=selector` + + const data = await withRetry( + async () => { + const response = await fetch(url) + if (!response.ok) { + throw new CCIPHttpError(response.status, response.statusText) + } + return (await response.json()) as ChainsAPIResponse + }, + { ...DEFAULT_API_RETRY_CONFIG, logger }, + ) + + cache.set(environment, { data, timestamp: Date.now() }) + return data +} + +/** + * Fetch chains from one or both environments. + * Uses exponential backoff for transient errors. + * @param environment - Optional environment filter ('mainnet' or 'testnet'). If not provided, fetches both. + * @param logger - Optional logger for retry attempts and warnings + * @returns Promise resolving to an array of API responses + */ +export async function fetchAllChains( + environment?: Environment, + logger?: Logger, +): Promise { + if (environment) { + return [await fetchChains(environment, logger)] + } + return Promise.all([fetchChains('mainnet', logger), fetchChains('testnet', logger)]) +} + +/** + * Search chains using the API's search parameter. + * The API auto-detects search type (displayName, selector, internalId). + * @param search - Search term + * @param environment - Optional environment filter ('mainnet' or 'testnet'). If not provided, searches both. + * @param logger - Optional logger for retry attempts + * @returns Promise resolving to an array of API responses + */ +export async function searchChainsAPI( + search: string, + environment?: Environment, + logger?: Logger, +): Promise { + const searchEnv = async (env: Environment): Promise => { + const url = `${API_BASE}?environment=${env}&outputKey=selector&search=${encodeURIComponent(search)}` + + return withRetry( + async () => { + const response = await fetch(url) + if (!response.ok) { + throw new CCIPHttpError(response.status, response.statusText) + } + return (await response.json()) as ChainsAPIResponse + }, + { ...DEFAULT_API_RETRY_CONFIG, logger }, + ) + } + + if (environment) { + return [await searchEnv(environment)] + } + return Promise.all([searchEnv('mainnet'), searchEnv('testnet')]) +} + +/** + * Flatten the nested API response structure into a flat array of ChainInfo objects. + * Uses SDK's networkInfo for name, family, and environment for consistency. + * @param responses - Array of API responses from fetchAllChains + * @returns Flat array of ChainInfo objects + */ +export function getAllChainsFlat(responses: ChainsAPIResponse[]): ChainInfo[] { + const chains: ChainInfo[] = [] + + for (const response of responses) { + const apiEnvironment = response.metadata.environment + for (const familyChains of Object.values(response.data)) { + for (const details of Object.values(familyChains)) { + // Use SDK networkInfo for consistent name, family, and environment + let name = details.internalId + let family = details.chainFamily.toUpperCase() + let environment: Environment = apiEnvironment + + try { + const info = networkInfo(BigInt(details.selector)) + name = info.name + family = info.family + environment = info.networkType === NetworkType.Testnet ? 'testnet' : 'mainnet' + } catch { + // Chain not in SDK - use API values + } + + chains.push({ + name, + chainId: details.chainId, + chainSelector: details.selector, + family, + displayName: details.displayName, + environment, + supported: details.supported, + }) + } + } + } + return chains +} + +/** + * Clear the cache for all environments. + */ +export function clearCache(): void { + cache.clear() +} diff --git a/ccip-cli/src/services/index.ts b/ccip-cli/src/services/index.ts new file mode 100644 index 00000000..b95cd804 --- /dev/null +++ b/ccip-cli/src/services/index.ts @@ -0,0 +1 @@ +export * from './docs-config-api.ts'