diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4e1afcb..2d350075 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: - name: Run lint run: npm run check - - name: Validate package exports + - name: Validate ccip-sdk package exports working-directory: ccip-sdk run: | # publint - check package.json exports and module format @@ -38,6 +38,16 @@ jobs: # Note: CJS resolution warnings are expected for ESM-only packages npx @arethetypeswrong/cli --pack . --profile esm-only || true + - name: Validate ccip-config package exports + working-directory: ccip-config + run: | + # publint - check package.json exports and module format + npx publint + + # attw - check TypeScript types resolution (informational) + # Note: CJS resolution warnings are expected for ESM-only packages + npx @arethetypeswrong/cli --pack . --profile esm-only || true + - name: Run tests with coverage run: | set -o pipefail diff --git a/CHANGELOG.md b/CHANGELOG.md index b5e5cea7..dbac03fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- SDK: Browser compatibility - explicit `buffer` dependency and imports for cross-platform support -- CI: Added `publint` and `@arethetypeswrong/cli` validation for package exports -- ESLint: `import/no-nodejs-modules` rule prevents Node.js-only imports in SDK -- Docs: Cross-Platform Portability guidelines in CONTRIBUTING.md +- **New Package**: `@chainlink/ccip-config` - Chain deployment registry with router addresses and display names + - Tree-shakable imports: `import '@chainlink/ccip-config/chains/evm/mainnet'` + - Lookup by SDK canonical name: `getDeploymentByName('ethereum-mainnet')` + - Lookup by selector: `getRouter()`, `requireRouter()`, `getDisplayName()` +- **CLI**: New `chains` command - list, search, and lookup chain info + - Filters: `--family`, `--mainnet`, `--testnet`, `--ccip-only` + - Search: `ccip chains --search arbitrum` + - Interactive: `ccip chains -i` +- **SDK**: Browser compatibility with explicit `buffer` imports +- **CI**: Added `publint` and `@arethetypeswrong/cli` for package validation ## [0.93.0] - 2025-12-26 - Pre-release diff --git a/README.md b/README.md index 12735805..6495dc79 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ TypeScript SDK and CLI for [CCIP](https://chain.link/cross-chain) (Cross-Chain I ## Packages -| Package | Description | Install | -| --------------------------------- | ----------------------------------- | ------------------------------------ | -| [@chainlink/ccip-sdk](./ccip-sdk) | TypeScript SDK for CCIP integration | `npm install @chainlink/ccip-sdk` | -| [@chainlink/ccip-cli](./ccip-cli) | Command-line interface | `npm install -g @chainlink/ccip-cli` | +| Package | Description | Install | +| ------------------------------------- | ------------------------------------ | --------------------------------------- | +| [@chainlink/ccip-sdk](./ccip-sdk) | TypeScript SDK for CCIP integration | `npm install @chainlink/ccip-sdk` | +| [@chainlink/ccip-cli](./ccip-cli) | Command-line interface | `npm install -g @chainlink/ccip-cli` | +| [@chainlink/ccip-config](./ccip-config) | Chain deployment configuration | `npm install @chainlink/ccip-config` | ## Quick Start diff --git a/ccip-cli/README.md b/ccip-cli/README.md index 57f491cf..e321298c 100644 --- a/ccip-cli/README.md +++ b/ccip-cli/README.md @@ -228,6 +228,50 @@ Attempts to parse hex-encoded function call data, error and revert reasons, for It'll recursively try to decode `returnData` and `error` arguments. +### `chains` + +```sh +ccip-cli chains [identifier] # List or lookup CCIP chain configuration +``` + +Discover and lookup CCIP chain information including router addresses. + +#### Options + +- `--family `: Filter by chain family +- `--mainnet`: Show only mainnets +- `--testnet`: Show only testnets +- `--ccip-only`: Show only CCIP-enabled chains (with router addresses) +- `--search ` / `-s `: Fuzzy search by name +- `--interactive` / `-i`: Interactive mode with type-ahead filtering +- `--json`: Output as JSON for scripting +- `--field `: Output only a specific field value +- `--count`: Show count summary only + +#### Examples + +```sh +# List all EVM mainnets with CCIP routers +ccip-cli chains --family evm --mainnet --ccip-only + +# Lookup a specific chain +ccip-cli chains ethereum-mainnet +ccip-cli chains 1 # by chainId +ccip-cli chains 5009297550715157269 # by selector + +# Fuzzy search (typo-tolerant) +ccip-cli chains --search "arbtrum" # finds "arbitrum" + +# Interactive mode for browsing +ccip-cli chains -i + +# Get just the router address for scripting +ccip-cli chains ethereum-mainnet --field router + +# JSON output +ccip-cli chains --json --family evm --mainnet +``` + ### `getSupportedTokens` ```sh diff --git a/ccip-cli/package.json b/ccip-cli/package.json index 22483a5f..81cd8c41 100644 --- a/ccip-cli/package.json +++ b/ccip-cli/package.json @@ -17,7 +17,7 @@ "typecheck": "tsc --noEmit", "check": "npm run lint && npm run typecheck", "build": "npm run clean && tsc -p ./tsconfig.build.json && npm run patch-dist", - "patch-dist": "chmod +x ./dist/index.js && find ./dist -type f -name \"*.js\" -exec sed -i.bkp 's|@chainlink/ccip-sdk/src/.*\\.ts|@chainlink/ccip-sdk|g' {} + && find ./dist -type f -name \"*.bkp\" -delete", + "patch-dist": "chmod +x ./dist/index.js && find ./dist -type f -name \"*.js\" -exec sed -i.bkp 's|@chainlink/ccip-sdk/src/.*\\.ts|@chainlink/ccip-sdk|g; s|@chainlink/ccip-config/src/index\\.ts|@chainlink/ccip-config|g; s|@chainlink/ccip-config/src/chains/evm/mainnet\\.ts|@chainlink/ccip-config/chains/evm/mainnet|g; s|@chainlink/ccip-config/src/chains/evm/testnet\\.ts|@chainlink/ccip-config/chains/evm/testnet|g; s|@chainlink/ccip-config/src/chains/solana/index\\.ts|@chainlink/ccip-config/chains/solana|g; s|@chainlink/ccip-config/src/chains/aptos/index\\.ts|@chainlink/ccip-config/chains/aptos|g; s|@chainlink/ccip-config/src/chains/sui/index\\.ts|@chainlink/ccip-config/chains/sui|g; s|@chainlink/ccip-config/src/chains/ton/index\\.ts|@chainlink/ccip-config/chains/ton|g' {} + && find ./dist -type f -name \"*.bkp\" -delete", "start": "./ccip-cli", "clean": "rm -rfv ./dist", "prepare": "npm run build" @@ -48,6 +48,7 @@ }, "dependencies": { "@aptos-labs/ts-sdk": "^5.2.0", + "@chainlink/ccip-config": "^0.93.0", "@chainlink/ccip-sdk": "^0.93.0", "@coral-xyz/anchor": "^0.29.0", "@ethers-ext/signer-ledger": "^6.0.0-beta.1", @@ -59,6 +60,7 @@ "@ton-community/ton-ledger": "^7.3.0", "bs58": "^6.0.0", "ethers": "6.16.0", + "fuse.js": "^7.1.0", "type-fest": "^5.3.1", "yargs": "18.0.0" }, diff --git a/ccip-cli/src/commands/chains.ts b/ccip-cli/src/commands/chains.ts new file mode 100644 index 00000000..7cf32993 --- /dev/null +++ b/ccip-cli/src/commands/chains.ts @@ -0,0 +1,277 @@ +/** + * 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, CCIP-enabled + * - Fuzzy search for typo-tolerant lookups + * - Interactive search with type-ahead filtering + * - JSON output for scripting + * - Field extraction for specific values + */ + +import { getAllDeployments } from '@chainlink/ccip-config/src/index.ts' +import { type ChainFamily, networkInfo } from '@chainlink/ccip-sdk/src/index.ts' +import { search } from '@inquirer/prompts' +import Fuse from 'fuse.js' +import type { Argv } from 'yargs' + +import '../config-loader.ts' +import type { GlobalOpts } from '../index.ts' +import { type Ctx, Format } from './types.ts' +import { getCtx, logParsedError } from './utils.ts' + +export const command = 'chains [identifier]' +export const describe = 'List and lookup CCIP chain configuration' + +type ChainsArgs = { + identifier?: string + family?: ChainFamily + mainnet?: boolean + testnet?: boolean + ccipOnly?: boolean + search?: string + interactive?: boolean + count?: boolean + json?: boolean + field?: string +} + +/** + * Yargs builder for the chains command. + * @param yargs - Yargs instance. + * @returns Configured yargs instance with command options. + */ +export const builder = (yargs: Argv): Argv => + yargs + .positional('identifier', { + type: 'string', + describe: 'Chain name, chainId, or selector to lookup', + }) + .options({ + family: { + type: 'string', + choices: ['evm', 'solana', 'aptos', 'sui', 'ton'] as const, + describe: 'Filter by chain family', + }, + mainnet: { type: 'boolean', describe: 'Show only mainnets' }, + testnet: { type: 'boolean', describe: 'Show only testnets' }, + 'ccip-only': { type: 'boolean', describe: 'Show only CCIP-enabled chains (with router)' }, + search: { alias: 's', type: 'string', describe: 'Fuzzy 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 +} + +type ChainInfo = { + name: string + chainId: number | string + chainSelector: bigint + family: ChainFamily + isTestnet: boolean + displayName: string + router?: string +} + +async function listChains( + ctx: Ctx, + argv: Awaited['argv']> & GlobalOpts, +) { + const { logger } = ctx + + // 1. If identifier provided, do single lookup + if (argv.identifier) { + try { + const network = networkInfo(argv.identifier) + const deployment = getAllDeployments().find((d) => d.chainSelector === network.chainSelector) + const result: ChainInfo = { + ...network, + displayName: deployment?.displayName ?? network.name, + router: deployment?.router, + } + + if (argv.field) { + const value = result[argv.field as keyof ChainInfo] + logger.log(value !== undefined ? String(value) : '') + return + } + if (argv.format === Format.json) { + logger.log(JSON.stringify(result, replacer, 2)) + return + } + // Pretty print + logger.log(`Name: ${result.name}`) + logger.log(`DisplayName: ${result.displayName}`) + logger.log(`Selector: ${result.chainSelector}`) + logger.log(`ChainId: ${result.chainId}`) + logger.log(`Family: ${result.family}`) + logger.log(`Testnet: ${result.isTestnet}`) + logger.log(`Router: ${result.router ?? '(not configured)'}`) + return + } catch { + logger.error(`Chain not found: ${argv.identifier}`) + process.exitCode = 1 + return + } + } + + // 2. Get all deployments from ccip-config + const deployments = getAllDeployments() + + // 3. Build chain info for each deployment + let chains: ChainInfo[] = deployments.map((d) => { + try { + const network = networkInfo(d.chainSelector) + return { + ...network, + displayName: d.displayName, + router: d.router, + } + } catch { + // If networkInfo fails, construct from deployment data + return { + name: d.displayName.toLowerCase().replace(/\s+/g, '-'), + chainId: 'unknown', + chainSelector: d.chainSelector, + family: 'evm' as ChainFamily, + isTestnet: + d.displayName.toLowerCase().includes('test') || + d.displayName.toLowerCase().includes('sepolia'), + displayName: d.displayName, + router: d.router, + } + } + }) + + // 4. Apply filters + if (argv.family) { + chains = chains.filter((n) => n.family === argv.family) + } + if (argv.mainnet) { + chains = chains.filter((n) => !n.isTestnet) + } + if (argv.testnet) { + chains = chains.filter((n) => n.isTestnet) + } + if (argv.ccipOnly) { + chains = chains.filter((n) => n.router !== undefined) + } + + // 5. Fuzzy search if provided + if (argv.search) { + const fuse = new Fuse(chains, { + keys: ['name', 'displayName'], + threshold: 0.4, // Allow typos + }) + chains = fuse.search(argv.search).map((r) => r.item) + } + + // 6. Output + if (argv.count) { + logger.log(chains.length) + return + } + + if (argv.format === Format.json) { + logger.log(JSON.stringify(chains, replacer, 2)) + return + } + + // 7. Interactive search mode + if (argv.interactive) { + const selected = await interactiveSearch(chains) + 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(`Testnet: ${selected.isTestnet}`) + logger.log(`Router: ${selected.router ?? '(not configured)'}`) + } + return + } + + // Table output + const nameWidth = Math.min(45, Math.max(20, ...chains.map((n) => n.displayName.length)) + 2) + const selectorWidth = 24 + const familyWidth = 10 + + logger.log( + 'Name'.padEnd(nameWidth) + + 'Selector'.padEnd(selectorWidth) + + 'Family'.padEnd(familyWidth) + + 'Router', + ) + logger.log('-'.repeat(nameWidth + selectorWidth + familyWidth + 44)) + + for (const n of chains) { + logger.log( + n.displayName.padEnd(nameWidth) + + String(n.chainSelector).padEnd(selectorWidth) + + n.family.padEnd(familyWidth) + + (n.router ?? '-'), + ) + } + logger.log(`\nTotal: ${chains.length} chains`) +} + +/** + * Interactive search with type-ahead filtering using inquirer/prompts. + * Allows users to filter chains as they type and select one to view details. + */ +async function interactiveSearch(chains: ChainInfo[]): Promise { + if (chains.length === 0) { + return undefined + } + + return search({ + message: 'Search and select a chain:', + pageSize: 15, + source: (term) => { + let filtered = chains + if (term) { + const fuse = new Fuse(chains, { + keys: ['name', 'displayName', 'chainId'], + threshold: 0.4, + }) + filtered = fuse.search(term).map((r) => r.item) + } + + const nameWidth = Math.min(30, Math.max(15, ...filtered.map((c) => c.displayName.length))) + const familyWidth = 8 + + return filtered.map((chain, i) => ({ + name: `${chain.displayName.padEnd(nameWidth)} ${chain.family.padEnd(familyWidth)} ${chain.router ? '✓' : '-'}`, + value: chain, + short: chain.displayName, + description: `${i + 1}/${filtered.length} | selector: ${chain.chainSelector}`, + })) + }, + }) +} diff --git a/ccip-cli/src/commands/send.ts b/ccip-cli/src/commands/send.ts index b179670d..bea44ccc 100644 --- a/ccip-cli/src/commands/send.ts +++ b/ccip-cli/src/commands/send.ts @@ -1,3 +1,4 @@ +import { requireRouter } from '@chainlink/ccip-config/src/index.ts' import { type AnyMessage, type CCIPVersion, @@ -7,7 +8,7 @@ import { CCIPArgumentInvalidError, CCIPChainFamilyUnsupportedError, CCIPTokenNotFoundError, - ChainFamily, + ChainFamily as ChainFamilyEnum, estimateExecGasForRequest, getDataBytes, networkInfo, @@ -16,14 +17,16 @@ import { import { type BytesLike, dataLength, formatUnits, toUtf8Bytes } from 'ethers' import type { Argv } from 'yargs' +import '../config-loader.ts' import type { GlobalOpts } from '../index.ts' import { showRequests } from './show.ts' import type { Ctx } from './types.ts' import { getCtx, logParsedError, parseTokenAmounts } from './utils.ts' import { fetchChainsFromRpcs, loadChainWallet } from '../providers/index.ts' -export const command = 'send ' -export const describe = 'Send a CCIP message from router on source to dest' +export const command = 'send ' +export const describe = + 'Send a CCIP message from source to dest (router auto-detected or use --router)' /** * Yargs builder for the send command. @@ -38,11 +41,6 @@ export const builder = (yargs: Argv) => describe: 'source network, chainId or name', example: 'ethereum-testnet-sepolia', }) - .positional('router', { - type: 'string', - demandOption: true, - describe: 'router contract address on source', - }) .positional('dest', { type: 'string', demandOption: true, @@ -50,6 +48,10 @@ export const builder = (yargs: Argv) => example: 'ethereum-testnet-sepolia-arbitrum-1', }) .options({ + router: { + type: 'string', + describe: 'Router contract address on source (auto-detected from ccip-config if omitted)', + }, receiver: { alias: 'R', type: 'string', @@ -156,6 +158,16 @@ async function sendMessage( const { logger } = ctx const sourceNetwork = networkInfo(argv.source) const destNetwork = networkInfo(argv.dest) + + // Resolve router: use provided value or auto-detect from ccip-config + let router: string + if (argv.router) { + router = argv.router + } else { + router = requireRouter(sourceNetwork.chainSelector) + logger.debug('Auto-detected router:', router) + } + const getChain = fetchChainsFromRpcs(ctx, argv) const source = await getChain(sourceNetwork.name) @@ -178,7 +190,7 @@ async function sendMessage( let tokenReceiver let accounts, accountIsWritableBitmap = 0n - if (destNetwork.family === ChainFamily.Solana) { + if (destNetwork.family === ChainFamilyEnum.Solana) { if (argv.tokenReceiver) tokenReceiver = argv.tokenReceiver else if (!tokenAmounts.length) { tokenReceiver = '11111111111111111111111111111111' @@ -219,12 +231,12 @@ async function sendMessage( if (argv.estimateGasLimit != null || argv.onlyEstimate) { // TODO: implement for all chain families - if (destNetwork.family !== ChainFamily.EVM) + if (destNetwork.family !== ChainFamilyEnum.EVM) throw new CCIPChainFamilyUnsupportedError(destNetwork.family, { context: { feature: 'gas estimation' }, }) const dest = (await getChain(destNetwork.chainSelector)) as unknown as EVMChain - const onRamp = await source.getOnRampForRouter(argv.router, destNetwork.chainSelector) + const onRamp = await source.getOnRampForRouter(router, destNetwork.chainSelector) const lane = { sourceChainSelector: source.network.chainSelector, destChainSelector: destNetwork.chainSelector, @@ -256,10 +268,10 @@ async function sendMessage( // `--allow-out-of-order-exec` forces EVMExtraArgsV2, which shouldn't work on v1.2 lanes; // otherwise, fallsback to EVMExtraArgsV1 (compatible with v1.2 & v1.5) const extraArgs = { - ...(argv.allowOutOfOrderExec != null || destNetwork.family !== ChainFamily.EVM + ...(argv.allowOutOfOrderExec != null || destNetwork.family !== ChainFamilyEnum.EVM ? { allowOutOfOrderExecution: !!argv.allowOutOfOrderExec } : {}), - ...(destNetwork.family === ChainFamily.Solana + ...(destNetwork.family === ChainFamilyEnum.Solana ? { computeUnits: BigInt(argv.gasLimit) } : { gasLimit: BigInt(argv.gasLimit) }), ...(tokenReceiver ? { tokenReceiver } : {}), @@ -272,7 +284,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 @@ -283,7 +295,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) } @@ -298,6 +310,7 @@ async function sendMessage( // calculate fee const fee = await source.getFee({ ...argv, + router, destChainSelector: destNetwork.chainSelector, message, }) @@ -316,6 +329,7 @@ async function sendMessage( if (!walletAddress) [walletAddress, wallet] = await loadChainWallet(source, argv) const request = await source.sendMessage({ ...argv, + router, destChainSelector: destNetwork.chainSelector, message: { ...message, fee }, wallet, diff --git a/ccip-cli/src/config-loader.ts b/ccip-cli/src/config-loader.ts new file mode 100644 index 00000000..03025f01 --- /dev/null +++ b/ccip-cli/src/config-loader.ts @@ -0,0 +1,23 @@ +/** + * Centralized chain deployment loader. + * Import this file to register all chain deployments with ccip-config. + * + * Usage: + * import './config-loader.ts' + * + * This enables features like: + * - Auto-detecting router addresses from chain selector + * - Looking up display names for chains + */ + +// EVM chains +import '@chainlink/ccip-config/src/chains/evm/mainnet.ts' +import '@chainlink/ccip-config/src/chains/evm/testnet.ts' + +// Non-EVM chains +import '@chainlink/ccip-config/src/chains/solana/index.ts' +import '@chainlink/ccip-config/src/chains/aptos/index.ts' +import '@chainlink/ccip-config/src/chains/sui/index.ts' +import '@chainlink/ccip-config/src/chains/ton/index.ts' + +export {} diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 40807c6c..7c1ea722 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.93.0-e6b317b' +const VERSION = '0.93.0-97194fa' // generate:end const globalOpts = { diff --git a/ccip-cli/src/providers/index.ts b/ccip-cli/src/providers/index.ts index 8ebca153..d71a3bcd 100644 --- a/ccip-cli/src/providers/index.ts +++ b/ccip-cli/src/providers/index.ts @@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises' import { type Chain, + type ChainFamily, type ChainGetter, type ChainTransaction, type EVMChain, @@ -11,7 +12,7 @@ import { CCIPNetworkFamilyUnsupportedError, CCIPRpcNotFoundError, CCIPTransactionNotFoundError, - ChainFamily, + ChainFamily as ChainFamilyEnum, networkInfo, supportedChains, } from '@chainlink/ccip-sdk/src/index.ts' @@ -201,19 +202,19 @@ export async function loadChainWallet(chain: Chain, argv: { wallet?: unknown; rp let wallet switch (chain.network.family) { - case ChainFamily.EVM: + case ChainFamilyEnum.EVM: wallet = await loadEvmWallet((chain as EVMChain).provider, argv) return [await wallet.getAddress(), wallet] as const - case ChainFamily.Solana: + case ChainFamilyEnum.Solana: wallet = await loadSolanaWallet(argv) return [wallet.publicKey.toBase58(), wallet] as const - case ChainFamily.Aptos: + case ChainFamilyEnum.Aptos: wallet = await loadAptosWallet(argv) return [wallet.accountAddress.toString(), wallet] as const - case ChainFamily.Sui: + case ChainFamilyEnum.Sui: wallet = loadSuiWallet(argv) return [wallet.toSuiAddress(), wallet] as const - case ChainFamily.TON: + case ChainFamilyEnum.TON: wallet = await loadTonWallet((chain as TONChain).provider, argv, chain.network.isTestnet) return [wallet.getAddress(), wallet] as const default: diff --git a/ccip-config/README.md b/ccip-config/README.md new file mode 100644 index 00000000..08fcdfce --- /dev/null +++ b/ccip-config/README.md @@ -0,0 +1,324 @@ +# @chainlink/ccip-config + +Chain deployment configuration registry for CCIP (Cross-Chain Interoperability Protocol). + +> [!IMPORTANT] +> This package is provided under an MIT license and is for convenience and illustration purposes only. + +## Overview + +This package provides deployment data (router addresses, display names) for CCIP-enabled chains. It is designed to work alongside `@chainlink/ccip-sdk`, which provides protocol-level chain information. + +**Separation of Concerns:** +- `@chainlink/ccip-sdk` - Protocol data (chain selectors, families, network info) +- `@chainlink/ccip-config` - Deployment data (router addresses, display names) + +## Installation + +```bash +npm install @chainlink/ccip-config +``` + +## Usage + +### Import Chain Deployments + +Chain deployments are registered via side-effect imports. Import only the chains you need: + +```typescript +// Import specific chain families/environments +import '@chainlink/ccip-config/chains/evm/mainnet' +import '@chainlink/ccip-config/chains/evm/testnet' +import '@chainlink/ccip-config/chains/solana/mainnet' + +// Or import all chains for a family +import '@chainlink/ccip-config/chains/evm' +import '@chainlink/ccip-config/chains/solana' + +// Or import everything +import '@chainlink/ccip-config/chains' +``` + +### Lookup Functions + +```typescript +import { getRouter, requireRouter, getDisplayName, isCCIPEnabled, isCCIPEnabledBySelector } from '@chainlink/ccip-config' +import '@chainlink/ccip-config/chains/evm/mainnet' + +// Get router address (returns undefined if not found) +const router = getRouter(5009297550715157269n) // Ethereum mainnet +// => '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D' + +// Get router address (throws if not found) +const router = requireRouter(5009297550715157269n) + +// Get display name +const name = getDisplayName(5009297550715157269n) +// => 'Ethereum' + +// Check if chain has CCIP router (by selector) +const enabled = isCCIPEnabledBySelector(5009297550715157269n) +// => true + +// Type guard for narrowing ChainDeployment to CCIPEnabledDeployment +const deployment = getDeployment(5009297550715157269n) +if (deployment && isCCIPEnabled(deployment)) { + // deployment.router is now string (not string | undefined) + console.log(deployment.router) +} +``` + +### Lookup by SDK Canonical Name + +```typescript +import { getDeploymentByName } from '@chainlink/ccip-config' +import '@chainlink/ccip-config/chains/evm/mainnet' + +// Find by SDK canonical name (case-sensitive, O(1) lookup) +const deployment = getDeploymentByName('ethereum-mainnet') +// => { chainSelector: 5009297550715157269n, name: 'ethereum-mainnet', displayName: 'Ethereum', router: '0x...' } + +// Names are case-sensitive (SDK canonical names are lowercase) +const notFound = getDeploymentByName('Ethereum') // undefined (display name, not SDK name) +``` + +### List All Deployments + +```typescript +import { getAllDeployments, getCCIPEnabledDeployments, getCCIPEnabledCount } from '@chainlink/ccip-config' +import '@chainlink/ccip-config/chains' + +// Get all registered deployments +const all = getAllDeployments() + +// Get only CCIP-enabled deployments (with router addresses) +const ccipEnabled = getCCIPEnabledDeployments() + +// Get count of CCIP-enabled chains (O(1) operation) +const count = getCCIPEnabledCount() +``` + +### With SDK Integration + +```typescript +import { networkInfo } from '@chainlink/ccip-sdk' +import { getRouter } from '@chainlink/ccip-config' +import '@chainlink/ccip-config/chains/evm/mainnet' + +// Get chain info from SDK +const network = networkInfo('ethereum-mainnet') + +// Get router from ccip-config +const router = getRouter(network.chainSelector) + +console.log(`${network.name} router: ${router}`) +``` + +### Isolated Registries (for Testing) + +```typescript +import { createRegistry } from '@chainlink/ccip-config' + +// Create an isolated registry that doesn't affect the global registry +const registry = createRegistry() + +// Register using a real chain selector (name auto-populated from SDK) +registry.register({ + chainSelector: 5009297550715157269n, // Ethereum mainnet + displayName: 'Ethereum', + router: '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D', +}) + +// Use the isolated registry +const deployment = registry.get(5009297550715157269n) +const byName = registry.getByName('ethereum-mainnet') // SDK canonical name, O(1) +const enabled = registry.getCCIPEnabled() +const count = registry.getCCIPEnabledCount() + +// For testing with fake chain selectors, use skipValidation +const testRegistry = createRegistry({ skipValidation: true }) +testRegistry.register({ chainSelector: 123n, displayName: 'Test Chain' }) + +// Clear when done +registry.clear() +``` + +```typescript +// With per-registry logger for full test isolation +const silentLogger = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +} +const registry = createRegistry({ logger: silentLogger }) +``` + +### Custom Logger + +By default, duplicate registrations log a warning via `console.warn`. You can customize this behavior with a logger that matches the SDK's Logger interface (`debug`, `info`, `warn`, `error` methods): + +```typescript +import { setLogger } from '@chainlink/ccip-config' + +// Use custom logger (all 4 methods required) +setLogger({ + debug: (msg) => myLogger.debug(msg), + info: (msg) => myLogger.info(msg), + warn: (msg) => myLogger.warning(msg), + error: (msg) => myLogger.error(msg), +}) + +// Suppress all logging (silent mode) +setLogger({ + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +}) +``` + +## API Reference + +### Lookup Functions + +| Function | Description | +|----------|-------------| +| `getRouter(selector)` | Get router address, returns `undefined` if not found | +| `getRouterByName(name)` | Get router by name, returns `undefined` if not found | +| `requireRouter(selector)` | Get router address, throws if not found | +| `getDisplayName(selector)` | Get display name, returns `undefined` if not found | +| `isCCIPEnabled(deployment)` | Type guard to check if deployment has router (narrows type) | +| `isCCIPEnabledBySelector(selector)` | Check if chain has router by selector | +| `getDeployment(selector)` | Get full deployment object | +| `requireDeployment(selector)` | Get deployment, throws if not found | +| `getDeploymentByName(name)` | Find deployment by SDK canonical name (case-sensitive, O(1)) | +| `requireDeploymentByName(name)` | Get deployment by SDK name, throws if not found | +| `requireRouterByName(name)` | Get router by SDK name, throws if not found or no router | +| `getAllDeployments()` | Get all registered deployments (returns frozen array) | +| `getCCIPEnabledDeployments()` | Get deployments with routers | +| `getCCIPEnabledCount()` | Get count of CCIP-enabled chains (O(1)) | + +### Registry Functions + +| Function | Description | +|----------|-------------| +| `createRegistry()` | Create an isolated registry instance | +| `registerDeployment(deployment)` | Register a deployment to global registry | +| `clearRegistry()` | Clear all deployments from global registry | +| `setLogger(logger)` | Set custom logger for duplicate registration warnings | + +### Types + +```typescript +// Input type for registration (name is auto-populated from SDK) +type ChainDeploymentInput = { + readonly chainSelector: bigint + readonly displayName: string + readonly router?: string +} + +// Full deployment type (with SDK canonical name) +type ChainDeployment = { + readonly chainSelector: bigint + readonly name: string // SDK canonical name (e.g., 'ethereum-mainnet') + readonly displayName: string // Human-readable name for UI + readonly router?: string +} + +type CCIPEnabledDeployment = ChainDeployment & { + readonly router: string +} + +interface Registry { + register(input: ChainDeploymentInput): void // Name auto-populated from SDK + get(chainSelector: bigint): ChainDeployment | undefined + getByName(name: string): ChainDeployment | undefined // SDK canonical name, O(1) + getRouter(chainSelector: bigint): string | undefined + getAll(): readonly ChainDeployment[] // Returns frozen array + getCCIPEnabled(): readonly CCIPEnabledDeployment[] // Returns frozen array + getCCIPEnabledCount(): number + clear(): void +} + +interface RegistryOptions { + logger?: Logger // Per-registry logger for full isolation + skipValidation?: boolean // For testing only +} + +interface Logger { + debug(...args: unknown[]): void + info(...args: unknown[]): void + warn(...args: unknown[]): void + error(...args: unknown[]): void +} +``` + +### Error Handling + +```typescript +import { + CCIPDeploymentNotFoundError, + CCIPDeploymentNotFoundByNameError, + CCIPRouterNotFoundError, + ErrorCodes +} from '@chainlink/ccip-config' + +try { + requireDeployment(123n) +} catch (e) { + if (e instanceof CCIPDeploymentNotFoundError) { + console.log(e.code) // 'CCIP_DEPLOYMENT_NOT_FOUND' + console.log(e.chainSelector) // 123n + console.log(e.recovery) // Suggestion to import chain data + } +} + +try { + requireDeploymentByName('Unknown Chain') +} catch (e) { + if (e instanceof CCIPDeploymentNotFoundByNameError) { + console.log(e.code) // 'CCIP_DEPLOYMENT_NOT_FOUND' + console.log(e.displayName) // 'Unknown Chain' + } +} +``` + +### Errors + +- `CCIPDeploymentNotFoundError` - Thrown when deployment not found for selector +- `CCIPDeploymentNotFoundByNameError` - Thrown when deployment not found for display name +- `CCIPRouterNotFoundError` - Thrown when router not configured for chain + +### Error Codes + +- `ErrorCodes.DEPLOYMENT_NOT_FOUND` - `'CCIP_DEPLOYMENT_NOT_FOUND'` +- `ErrorCodes.ROUTER_NOT_FOUND` - `'CCIP_ROUTER_NOT_FOUND'` + +## Available Chain Imports + +| Import Path | Description | +|-------------|-------------| +| `@chainlink/ccip-config/chains` | All chains (all families, mainnet + testnet) | +| `@chainlink/ccip-config/chains/evm` | All EVM chains | +| `@chainlink/ccip-config/chains/evm/mainnet` | EVM mainnets only | +| `@chainlink/ccip-config/chains/evm/testnet` | EVM testnets only | +| `@chainlink/ccip-config/chains/solana` | All Solana chains | +| `@chainlink/ccip-config/chains/solana/mainnet` | Solana mainnet only | +| `@chainlink/ccip-config/chains/solana/testnet` | Solana testnets only | +| `@chainlink/ccip-config/chains/aptos` | All Aptos chains | +| `@chainlink/ccip-config/chains/sui` | All Sui chains | +| `@chainlink/ccip-config/chains/ton` | All TON chains | + +## Tree Shaking + +This package is designed for tree-shaking. Only the chains you import will be included in your bundle: + +```typescript +// Only EVM mainnet chains included in bundle +import '@chainlink/ccip-config/chains/evm/mainnet' +``` + +## License + +MIT diff --git a/ccip-config/package.json b/ccip-config/package.json new file mode 100644 index 00000000..0fd0b518 --- /dev/null +++ b/ccip-config/package.json @@ -0,0 +1,122 @@ +{ + "name": "@chainlink/ccip-config", + "version": "0.93.0", + "description": "CCIP chain configuration and registry", + "author": "Chainlink devs", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/smartcontractkit/ccip-tools-ts.git" + }, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./chains": { + "types": "./dist/chains/index.d.ts", + "default": "./dist/chains/index.js" + }, + "./chains/evm": { + "types": "./dist/chains/evm/index.d.ts", + "default": "./dist/chains/evm/index.js" + }, + "./chains/evm/mainnet": { + "types": "./dist/chains/evm/mainnet.d.ts", + "default": "./dist/chains/evm/mainnet.js" + }, + "./chains/evm/testnet": { + "types": "./dist/chains/evm/testnet.d.ts", + "default": "./dist/chains/evm/testnet.js" + }, + "./chains/solana": { + "types": "./dist/chains/solana/index.d.ts", + "default": "./dist/chains/solana/index.js" + }, + "./chains/solana/mainnet": { + "types": "./dist/chains/solana/mainnet.d.ts", + "default": "./dist/chains/solana/mainnet.js" + }, + "./chains/solana/testnet": { + "types": "./dist/chains/solana/testnet.d.ts", + "default": "./dist/chains/solana/testnet.js" + }, + "./chains/aptos": { + "types": "./dist/chains/aptos/index.d.ts", + "default": "./dist/chains/aptos/index.js" + }, + "./chains/aptos/mainnet": { + "types": "./dist/chains/aptos/mainnet.d.ts", + "default": "./dist/chains/aptos/mainnet.js" + }, + "./chains/aptos/testnet": { + "types": "./dist/chains/aptos/testnet.d.ts", + "default": "./dist/chains/aptos/testnet.js" + }, + "./chains/sui": { + "types": "./dist/chains/sui/index.d.ts", + "default": "./dist/chains/sui/index.js" + }, + "./chains/sui/mainnet": { + "types": "./dist/chains/sui/mainnet.d.ts", + "default": "./dist/chains/sui/mainnet.js" + }, + "./chains/sui/testnet": { + "types": "./dist/chains/sui/testnet.d.ts", + "default": "./dist/chains/sui/testnet.js" + }, + "./chains/ton": { + "types": "./dist/chains/ton/index.d.ts", + "default": "./dist/chains/ton/index.js" + }, + "./chains/ton/mainnet": { + "types": "./dist/chains/ton/mainnet.d.ts", + "default": "./dist/chains/ton/mainnet.js" + }, + "./chains/ton/testnet": { + "types": "./dist/chains/ton/testnet.d.ts", + "default": "./dist/chains/ton/testnet.js" + }, + "./src/*": "./src/*" + }, + "sideEffects": [ + "./dist/chains/**/*.js", + "./src/chains/**/*.ts" + ], + "scripts": { + "test": "node --test", + "lint": "prettier --check ./src && eslint ./src", + "lint:fix": "prettier --write ./src && eslint --fix ./src", + "typecheck": "tsc --noEmit", + "check": "npm run lint && npm run typecheck", + "build": "npm run clean && tsc -p ./tsconfig.build.json", + "clean": "rm -rfv ./dist", + "prepare": "npm run build" + }, + "files": [ + "dist", + "src", + "tsconfig.json", + "!**/*.test.*", + "!**/__tests__", + "!**/__mocks__" + ], + "dependencies": { + "@chainlink/ccip-sdk": "^0.93.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.2", + "@types/node": "25.0.3", + "eslint": "^9.39.2", + "eslint-config-prettier": "10.1.8", + "eslint-import-resolver-typescript": "4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsdoc": "^61.5.0", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-tsdoc": "^0.5.0", + "prettier": "^3.7.4", + "typescript": "5.9.3", + "typescript-eslint": "8.51.0" + } +} diff --git a/ccip-config/src/chains/aptos/index.ts b/ccip-config/src/chains/aptos/index.ts new file mode 100644 index 00000000..f06930a1 --- /dev/null +++ b/ccip-config/src/chains/aptos/index.ts @@ -0,0 +1,3 @@ +// Side-effect imports to register Aptos chain deployments +import './mainnet.ts' +import './testnet.ts' diff --git a/ccip-config/src/chains/aptos/mainnet.ts b/ccip-config/src/chains/aptos/mainnet.ts new file mode 100644 index 00000000..9b33b21c --- /dev/null +++ b/ccip-config/src/chains/aptos/mainnet.ts @@ -0,0 +1,18 @@ +import { registerDeployment } from '../../registry.ts' + +// CCIP Aptos Mainnet deployments +// Only contains: chainSelector, displayName, router +// Protocol data (chainId, name, family, isTestnet) lives in the SDK + +// [chainSelector, displayName, router?] +export const chains: readonly [bigint, string, string?][] = [ + [ + 4741433654826277614n, + 'Aptos', + '0x20f808de3375db34d17cc946ec6b43fc26962f6afa125182dc903359756caf6b', + ], +] + +for (const [chainSelector, displayName, router] of chains) { + registerDeployment({ chainSelector, displayName, router }) +} diff --git a/ccip-config/src/chains/aptos/testnet.ts b/ccip-config/src/chains/aptos/testnet.ts new file mode 100644 index 00000000..58a56195 --- /dev/null +++ b/ccip-config/src/chains/aptos/testnet.ts @@ -0,0 +1,19 @@ +import { registerDeployment } from '../../registry.ts' + +// CCIP Aptos Testnet/Localnet deployments +// Only contains: chainSelector, displayName, router +// Protocol data (chainId, name, family, isTestnet) lives in the SDK + +// [chainSelector, displayName, router?] +export const chains: readonly [bigint, string, string?][] = [ + [4457093679053095497n, 'Aptos Localnet'], + [ + 743186221051783445n, + 'Aptos Testnet', + '0xc748085bd02022a9696dfa2058774f92a07401208bbd34cfd0c6d0ac0287ee45', + ], +] + +for (const [chainSelector, displayName, router] of chains) { + registerDeployment({ chainSelector, displayName, router }) +} diff --git a/ccip-config/src/chains/evm/index.ts b/ccip-config/src/chains/evm/index.ts new file mode 100644 index 00000000..6b06e0b1 --- /dev/null +++ b/ccip-config/src/chains/evm/index.ts @@ -0,0 +1,3 @@ +// Side-effect imports to register EVM chain deployments +import './mainnet.ts' +import './testnet.ts' diff --git a/ccip-config/src/chains/evm/mainnet.ts b/ccip-config/src/chains/evm/mainnet.ts new file mode 100644 index 00000000..c9dc3b0b --- /dev/null +++ b/ccip-config/src/chains/evm/mainnet.ts @@ -0,0 +1,120 @@ +import { registerDeployment } from '../../registry.ts' + +// CCIP Mainnet EVM deployments +// Only contains: chainSelector, displayName, router +// Protocol data (chainId, name, family, isTestnet) lives in the SDK + +// [chainSelector, displayName, router?] +export const chains: readonly [bigint, string, string?][] = [ + [4426351306075016396n, '0G', '0x0aA145a62153190B8f0D3cA00c441e451529f755'], + [4829375610284793157n, 'AB Chain', '0x492641F648a4986844848E0beFE66D14817bCE34'], + [3577778157919314504n, 'Abstract', '0x09521B0B5BB2d4406124c0207Cf551829B45f84d'], + [14894068710063348487n, 'Apechain', '0xe9c6945281028cb6530d43F998eE539dFE2a9191'], + [1939936305787790600n, 'Areon Mainnet'], + [6433500567565415381n, 'Avalanche', '0xF4c7E640EdA248ef95972845a62bdC74237805dB'], + [5463201557265485081n, 'Avalanche Subnet Dexalot Mainnet'], + [1294465214383781161n, 'Berachain', '0x71a275704c283486fBa26dad3dd0DB78804426eF'], + [11344663589394136015n, 'BNB Chain', '0x34B03Cb9086d7D758AC55af71584F81A598759FE'], + [465944652040885897n, 'opBNB', '0xa3ca4306B9256aAB177C47A18b43593F03378976'], + [4874388048629246000n, 'Bitcichain Mainnet'], + [7937294810946806131n, 'Bitlayer', '0x6c0aA29330c58dda07faD577fF5a0280823a910c'], + [3849287863852499584n, 'BOB', '0x827716e74F769AB7b6bb374A29235d9c2156932C'], + [4560701533377838164n, 'Botanix', '0x5EE890c89B5Ae75cBC516Dd53345e38E5B39B664'], + [5406759801798337480n, 'B²', '0x9C34e9A192d7a4c2cf054668C1122C028C43026c'], + [241851231317828981n, 'Merlin', '0x8Be462D21b05eEeF81a3AA384b7C6CF18597232A'], + [2135107236357186872n, 'Bittensor EVM', '0xD941fBEcD2b971d0F54b4C34286C95faB52B60B8'], + [3776006016387883143n, 'Bittorrent_chain Mainnet'], + [1346049177634351622n, 'Celo', '0xfB48f15480926A4ADf9116Dca468bDd2EE6C5F62'], + [9478124434908827753n, 'Codex Mainnet'], + [1761333065194157300n, 'Coinex_smart_chain Mainnet'], + [3358365939762719202n, 'Conflux Mainnet'], + [1224752112135636129n, 'Core', '0xF7Cc8b0B5263A74AFBb1a2ac87FfF1CF7E62152f'], + [9043146809313071210n, 'Corn', '0x183f6069A0D5c2DEC1Dd1eCF3B1581e12dEb4Efe'], + [1456215246176062136n, 'Cronos', '0xE26B0A098D861d5C7d9434aD471c0572Ca6EAa67'], + [8788096068760390840n, 'Cronos zkEVM', '0x17b828DF8679D68318f0849C1221AD1760699eCb'], + [5009297550715157269n, 'Ethereum', '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D'], + [4949039107694359620n, 'Arbitrum One', '0x141fa059441E0ca23ce184B6A78bafD2A517DdE8'], + [3162193654116181371n, 'Ethereum Mainnet Arbitrum 1 L3x 1'], + [1010349088906777999n, 'Ethereum Mainnet Arbitrum 1 Treasure 1'], + [1540201334317828111n, 'Ethereum Mainnet Astar ZKEVM 1'], + [15971525489660198786n, 'Base', '0x881e3A65B4d4a04dD529061dd0071cf975F58bCD'], + [4411394078118774322n, 'Blast', '0x12e0B8E349C6fb7E6E40713E8125C3cF7E62152f'], + [7613811247471741961n, 'HashKey Chain', '0xf2Fd62c083F3BF324e99ce157D1a42d7EbA77f1d'], + [1237925231416731909n, 'Ethereum Mainnet Immutable ZKEVM 1'], + [3461204551265785888n, 'Ink', '0xca7c90A52B44E301AC01Cb5EB99b2fD99339433A'], + [3719320017875267166n, 'Ethereum Mainnet Kroma 1'], + [4627098889531055414n, 'Linea', '0x549FEB73F2348F6cD99b9fc8c69252034897f06C'], + [1556008542357238666n, 'Mantle', '0x670052635a9850bb45882Cb2eCcF66bCff0F41B7'], + [8805746078405598895n, 'Metis Andromeda', '0x7b9FB8717D306e2e08ce2e1Efa81F026bf9AD13c'], + [7264351850409363825n, 'Mode', '0x24C40f13E77De2aFf37c280BA06c333531589bf1'], + [3734403246176062136n, 'OP', '0x3206695CaE29952f4b0c22a169725a865bc8Ce0f'], + [4348158687435793198n, 'Polygon zkEVM', '0xA9999937159B293c72e2367Ce314cb3544e7C1a3'], + [13204309965629103672n, 'Scroll', '0x9a55E8Cab6564eb7bbd7124238932963B8Af71DC'], + [16468599424800719238n, 'Taiko Alethia', '0xeb2502AeD3Cfd6E37e292c6B837a8FFF9a042367'], + [1923510103922296319n, 'Unichain', '0x68891f5F96695ECd7dEdBE2289D1b73426ae7864'], + [2049429975587534727n, 'World Chain', '0x5fd9E4986187c56826A3064954Cfa2Cf250cfA0f'], + [3016212468291539606n, 'X Layer', '0xF2b6Cb7867EB5502C3249dD37D7bc1Cc148e5232'], + [17198166215261833993n, 'Zircuit', '0x0A6436B56378D305729713ac332ccdCD367f3918'], + [1562403441176082196n, 'ZKsync', '0x748Fd769d81F5D94752bf8B0875E9301d0ba71bB'], + [13624601974233774587n, 'Etherlink', '0x1912C3cFafE8A76A32a92861d815aC2837F237Ca'], + [9723842205701363942n, 'Everclear', '0x54fC28aa6DBf53277a7E5F4c789F823b86b9f781'], + [3768048213127883732n, 'Fantom Mainnet'], + [4561443241176882990n, 'Filecoin Mainnet'], + [1462016016387883143n, 'Fraxtal', '0x4bdF20477744Ec5F9DE738b5cC9ACd01763905ee'], + [9688382747979139404n, 'Gate Chain Mainnet'], + [9373518659714509671n, 'Gate Layer Mainnet'], + [465200170687744372n, 'Gnosis', '0x4aAD6071085df840abD9Baf1697d5D5992bDadce'], + [3229138320728879060n, 'Hedera', '0x87b400B4d4F5Fe2Fdb6FBEa66C38003ced565b76'], + [1804312132722180201n, 'Hemi', '0x5e48912cFDd14417D6856872341f894AE0EF07DD'], + [2442541497099098535n, 'HyperEVM', '0x13b3332b66389B1467CA6eBd6fa79775CCeF65ec'], + [9107126442626377432n, 'Janction Mainnet'], + [1523760397290643893n, 'Jovay', '0x492641F648a4986844848E0beFE66D14817bCE34'], + [9813823125703490621n, 'Kaia', '0x4Eb2a60AF37bC6bb05500F581c00E8EA3075f6E9'], + [7550000543357438061n, 'Kava Mainnet'], + [1355020143337428062n, 'Kusama Mainnet Moonriver'], + [5608378062013572713n, 'Lens', '0x498F3feBAd3ff75e05b7847B37a301fc2DA6fDC0'], + [15293031020466096408n, 'Lisk', '0x0145c1fbA8a16128c1061eB9CE7eC3cadb8e30c7'], + [6093540873831549674n, 'Megaeth Mainnet'], + [6473245816409426016n, 'Memento', '0x492641F648a4986844848E0beFE66D14817bCE34'], + [13447077090413146373n, 'Metal L2', '0x020c61ECEEE0E5DC32F2503AbB6E070fa0EbBfaA'], + [11690709103138290329n, 'Mind Network', '0x3E13485E767D53f938cD4AF502111d3fF8726A2D'], + [17164792800244661392n, 'Mint', '0x1d86012266F214a368766C2B9329FdCC75B1Ce6b'], + [8481857512324358265n, 'Monad', '0x33566fE5976AAa420F3d5C64996641Fc3858CaDB'], + [18164309074156128038n, 'Morph', '0x3201a20D2a33820C0DaC8Bc93C4819755C2a8c7F'], + [2039744413822257700n, 'Near Mainnet'], + [8239338020728974000n, 'Neonlink Mainnet'], + [7222032299962346917n, 'Neox Mainnet'], + [12657445206920369324n, 'Henesys', '0x492641F648a4986844848E0beFE66D14817bCE34'], + [15758750456714168963n, 'Nexon Mainnet Lith'], + [17349189558768828726n, 'Nibiru Mainnet'], + [9335212494177455608n, 'Plasma', '0xcDca5D374e46A6DDDab50bD2D9acB8c796eC35C3'], + [17912061998839310979n, 'Plume', '0x5C4f4622AD0EC4a47e04840db7E9EcA8354109af'], + [6422105447186081193n, 'Astar', '0x8D5c5CB8ec58285B424C93436189fB865e437feF'], + [8175830712062617656n, 'Polkadot Mainnet Centrifuge'], + [8866418665544333000n, 'Polkadot Mainnet Darwinia'], + [1252863800116739621n, 'Polkadot Mainnet Moonbeam'], + [4051577828743386545n, 'Polygon', '0x849c5ED5a80F5B408Dd4969b78c2C8fdf0565Bfe'], + [2459028469735686113n, 'Katana', '0x7c19b79D2a054114Ab36ad758A36e92376e267DA'], + [6916147374840168594n, 'Ronin', '0x46527571D5D1B68eE7Eb60B18A32e6C60DcEAf99'], + [11964252391146578476n, 'Rootstock', '0xCe7aFb0BF5F73BfDB5e9E04976eBac2005746bD0'], + [9027416829622342829n, 'Sei Network', '0xAba60dA7E88F7E8f5868C2B6dE06CB759d693af0'], + [3993510008929295315n, 'Shibarium', '0xc2CA5d5C17911e4B838194b51585DdF8fe5116C1'], + [12505351618335765396n, 'Soneium', '0x8C8B88d827Fe14Df2bc6392947d513C86afD6977'], + [1673871237479749969n, 'Sonic', '0xB4e1Ff7882474BB93042be9AD5E1fA387949B860'], + [16978377838628290997n, 'Stable', '0xECFF67559c0583027A5fbd85136E33bC4D66eeA0'], + [470401360549526817n, 'Superseed', '0xAD93FBB3A9a077F896e1F57739e43dEd063f181F'], + [5936861837188149645n, 'Tac', '0x966519C334D895121B61584CAdeBc15571b62983'], + [1477345371608778000n, 'Telos EVM Mainnet'], + [5214452172935136222n, 'Treasure Mainnet'], + [1546563616611573946n, 'Tron Mainnet EVM'], + [374210358663784372n, 'Velas Mainnet'], + [5142893604156789321n, 'Wemix', '0x7798b795Fde864f4Cd1b124a38Ba9619B7F8A442'], + [17673274061779414707n, 'XDC Network', '0x2a9f896660E802c59a3178b2E8CB7FBaCCC04e86'], + [10817664450262215148n, 'Zetachain Mainnet'], + [4350319965322101699n, 'Zklink_nova Mainnet'], + [3555797439612589184n, 'Zora', '0x65b40941fa86Fc444043257cd677a7F0bD034F79'], +] + +for (const [chainSelector, displayName, router] of chains) { + registerDeployment({ chainSelector, displayName, router }) +} diff --git a/ccip-config/src/chains/evm/testnet.ts b/ccip-config/src/chains/evm/testnet.ts new file mode 100644 index 00000000..b8c83f49 --- /dev/null +++ b/ccip-config/src/chains/evm/testnet.ts @@ -0,0 +1,160 @@ +import { registerDeployment } from '../../registry.ts' + +// CCIP Testnet EVM deployments +// Only contains: chainSelector, displayName, router +// Protocol data (chainId, name, family, isTestnet) lives in the SDK + +// [chainSelector, displayName, router?] +export const chains: readonly [bigint, string, string?][] = [ + [2131427466778448014n, '0G Galileo Testnet', '0x5c21Bb4Bd151Bd6Fa2E6d7d1b63B83485529Cdb4'], + [6892437333620424805n, '0g Testnet Galileo 1'], + [16088006396410204581n, '0g Testnet Newton'], + [7051849327615092843n, 'Ab Testnet'], + [16235373811196386733n, 'Abstract Sepolia', '0xC308ef8a02e39887CCF55a796a128CBD1F2072a1'], + [7759470850252068959n, 'Anvil Devnet'], + [9900119385908781505n, 'Apechain Curtis', '0x6139Bd336bebFaaCbca33D183CeD1C90B62500cB'], + [3034092155422581607n, 'Arc Testnet'], + [7317911323415911000n, 'Areon Testnet'], + [1458281248224512906n, 'Avalanche Subnet Dexalot Testnet'], + [14767482510784806043n, 'Avalanche Fuji', '0xF694E193200268f9a4868e4Aa017A0118C9a8177'], + [7837562506228496256n, 'Avalanche Testnet Nexon'], + [12336603543561911511n, 'Berachain Testnet Artio'], + [8999465244383784164n, 'Berachain Bartio', '0xb1653462481e1bF30B5cca3082e2454E41668c65'], + [7728255861635209484n, 'Berachain Testnet Bepolia'], + [13264668187771770619n, 'BNB Chain Testnet', '0xE1053aE1857476f36A3C62580FF9b016E8EE8F6f'], + [13274425992935471758n, 'opBNB Testnet', '0xD9182959D9771cc77e228cB3caFe671f45A37630'], + [4888058894222120000n, 'Bitcichain Testnet'], + [3789623672476206327n, 'Bitlayer Testnet', '0x3dfbe078277609D34c8ef015c61f23A9BeDE61BB'], + [1467223411771711614n, 'Botanix Testnet', '0x8a27438666Ef45093802F869bd146fB183dd5A32'], + [1948510578179542068n, 'B² Testnet', '0x34A49Eb641daF64d61be00Aa7F759f8225351101'], + [5269261765892944301n, 'Merlin Testnet', '0x500063c3827cd871E7b4Af0384E369bDEb75b2e2'], + [8953668971247136127n, 'Rootstock Testnet', '0xfEE82327fC68cE497283159Eb724Ba7427b097e3'], + [5535534526963509396n, 'BOB Sepolia', '0x7808184405d6Cbc663764003dE21617fa640bc82'], + [2177900824115119161n, 'Bittensor Testnet'], + [4459371029167934217n, 'Bittorrent_chain Testnet'], + [3761762704474186180n, 'Celo Sepolia'], + [3552045678561919002n, 'Celo Alfajores', '0xb00E95b773528E2Ea724DB06B75113F239D15Dca'], + [7225665875429174318n, 'Codex Testnet'], + [8955032871639343000n, 'Coinex_smart_chain Testnet'], + [4264732132125536123n, 'Core Testnet', '0xded0EE188Fe8F1706D9049e29C82081A5ebEcb2F'], + [2995292832068775165n, 'Cronos Testnet', '0xa0F5f5867F528CCc0f9bCc5225063b4A38b5dEBd'], + [3842103497652714138n, 'Cronos Testnet ZKEVM 1'], + [16487132492576884721n, 'Cronos zkEVM Testnet', '0xFeFC5B70DA3297A8470e4D0D2Ea85E0F63bA6b0c'], + [15513093881969820114n, 'Dtcc Testnet Andesite'], + [6101244977088475029n, 'Ethereum Testnet Goerli Arbitrum 1'], + [5790810961207155433n, 'Ethereum Testnet Goerli Base 1'], + [1355246678561316402n, 'Ethereum Testnet Goerli Linea 1'], + [4168263376276232250n, 'Ethereum Testnet Goerli Mantle 1'], + [2664363617261496610n, 'Ethereum Testnet Goerli Optimism 1'], + [11059667695644972511n, 'Ethereum Testnet Goerli Polygon ZKEVM 1'], + [6802309497652714138n, 'Ethereum Testnet Goerli Zksync 1'], + [7717148896336251131n, 'Holesky', '0xb9531b46fE8808fB3659e39704953c2B1112DD43'], + [8901520481741771655n, 'Fraxtal Testnet', '0x0a355FC36C10007D3059637f0cd7cFfBE845241a'], + [8304510386741731151n, 'Ethereum Testnet Holesky Morph 1'], + [7248756420937879088n, 'Taiko Hekla', '0x07a2b9BB0456a7e999B61ca8F166ADDF5878F468'], + [10380998176179737091n, 'Ethereum Hoodi', '0xc93Dac3422660A41500a24C94BF14616995e3CA6'], + [1064004874793747259n, 'Morph Hoodi', '0xd1CBe8dF481C7a78AaaAfB0466814d13d93bd9b7'], + [9873759436596923887n, 'Ethereum Testnet Hoodi Taiko'], + [15858691699034549072n, 'Ethereum Testnet Hoodi Taiko 1'], + [16015286601757825753n, 'Ethereum Sepolia', '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59'], + [3478487238524512106n, 'Arbitrum Sepolia', '0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165'], + [3486622437121596122n, 'Ethereum Testnet Sepolia Arbitrum 1 L3x 1'], + [10443705513486043421n, 'Ethereum Testnet Sepolia Arbitrum 1 Treasure 1'], + [10344971235874465080n, 'Base Sepolia', '0xD3b06cEbF099CE7DA4AcCf578aaebFDBd6e88a93'], + [2027362563942762617n, 'Blast Sepolia', '0xfb2f2A207dC428da81fbAFfDDe121761f8Be1194'], + [1467427327723633929n, 'Corn Testnet', '0x9981250f56d4d0Fa9736343659B4890ebbb94110'], + [4356164186791070119n, 'HashKey Chain Testnet', '0x1360c71dd2458B6d4A5Ad5946d9011BafA0435d7'], + [4526165231216331901n, 'Ethereum Testnet Sepolia Immutable ZKEVM 1'], + [5990477251245693094n, 'Kroma Sepolia', '0xA8C0c11bf64AF62CDCA6f93D3769B88BdD7cb93D'], + [6827576821754315911n, 'Lens Sepolia', '0xf5Aa9fe2B78d852490bc4E4Fe9ab19727DD10298'], + [5719461335882077547n, 'Linea Sepolia', '0xB4431A6c63F72916151fEA2864DBB13b8ce80E8a'], + [5298399861320400553n, 'Lisk Sepolia', '0x78805d2881d233a430983Dbc170990AefDe60C93'], + [8236463271206331221n, 'Mantle Sepolia', '0xFd33fd627017fEf041445FC19a2B6521C9778f86'], + [3777822886988675105n, 'Metis Sepolia', '0xaCdaBa07ECad81dc634458b98673931DD9d3Bc14'], + [829525985033418733n, 'Mode Sepolia', '0xc49ec0eB4beb48B8Da4cceC51AA9A5bD0D0A4c43'], + [5224473277236331295n, 'OP Sepolia', '0x114A20A10b43D4115e5aeef7345a1A71d2a60C57'], + [4418231248214522936n, 'Ethereum Testnet Sepolia Polygon Validium 1'], + [1654667687261492630n, 'Polygon zkEVM Cardona', '0x91A7f913EEF5E3058AD1Bf8842C294f7219C7271'], + [2279865765895943307n, 'Scroll Sepolia', '0x6aF501292f2A33C81B9156203C9A66Ba0d8E3D21'], + [686603546605904534n, 'Soneium Minato', '0x443a1bce545d56E2c3f20ED32eA588395FFce0f4'], + [14135854469784514356n, 'Unichain Sepolia', '0x5b7D7CDf03871dc9Eb00830B027e70A75bd3DC95'], + [5299555114858065850n, 'World Chain Sepolia', '0x47693fc188b2c30078F142eadc2C009E8D786E8d'], + [2066098519157881736n, 'X Layer Testnet', '0xc5F5330C4793AF46872a9eC15b76a007A96a4152'], + [4562743618362911021n, 'Ethereum Testnet Sepolia Zircuit 1'], + [6898391096552792247n, 'ZKsync Sepolia', '0xA1fdA8aa9A8C4b945C45aD30647b01f07D7A0B16'], + [1910019406958449359n, 'Etherlink Testnet', '0xa1312a58873fb9a16008E259c3eB972038ba46D9'], + [379340054879810246n, 'Everclear Testnet Sepolia'], + [4905564228793744293n, 'Fantom Testnet'], + [7060342227814389000n, 'Filecoin Testnet'], + [3558960680482140165n, 'Gate Chain Testnet Meteora'], + [3667207123485082040n, 'Gate Layer Testnet'], + [3379446385462418246n, 'Geth Testnet'], + [8871595565390010547n, 'Gnosis Chiado', '0x19b1bac554111517831ACadc0FD119D23Bb14391'], + [222782988166878823n, 'Hedera Testnet', '0x802C5F84eAD128Ff36fD6a3f8a418e339f467Ce4'], + [16126893759944359622n, 'Hemi Sepolia', '0xc1D615EC997F581741d867B08bC7050c90d213B0'], + [4286062357653186312n, 'Hyperliquid Testnet'], + [9763904284804119144n, 'Ink Sepolia', '0x17fCda531D8E43B4e2a2A2492FBcd4507a1685A1'], + [5059197667603797935n, 'Janction Testnet', '0x6ddAFdf8bA76AFED73d6e7B599adDE014fA293bC'], + [945045181441419236n, 'Jovay Sepolia Testnet', '0x2016AA303B331bd739Fd072998e579a3052500A6'], + [2624132734533621656n, 'Kaia Kairos', '0x41477416677843fCE577748D2e762B6638492755'], + [2110537777356199208n, 'Kava Testnet'], + [2443239559770384419n, 'MegaETH Testnet', '0x35e752bf853009C8E080CdB3De88e3273B1c75E3'], + [18241817625092392675n, 'Megaeth Testnet 2'], + [12168171414969487009n, 'Memento Testnet', '0xEAB080c724587fFC9F2EFF82e36EE4Fb27774959'], + [6286293440461807648n, 'Metal L2 Testnet', '0xB6F0d42A356aD4DC890BCD3EdCAFb1df13a30777'], + [7189150270347329685n, 'Mind Network Testnet', '0xf877Eb80E5Ab0d58afF1a1431756B74Dd5190021'], + [10749384167430721561n, 'Mint Sepolia', '0x6Ce68Fb8eA7376d5c84de5486dE46286F6Dd3e36'], + [2183018362218727504n, 'Monad Testnet', '0x5f16e51e3Dcb255480F090157DD01bA962a53E54'], + [5061593697262339000n, 'Near Testnet'], + [1113014352258747600n, 'Neonlink Testnet'], + [2217764097022649312n, 'Neo X Testnet T4', '0x609747816B6C237d5C4960065BC11d2F0DE752A6'], + [8911150974185440581n, 'Nexon Dev'], + [14632960069656270105n, 'Nexon Qa'], + [5556806327594153475n, 'Nexon Stage'], + [305104239123120457n, 'Nibiru Testnet'], + [344208382356656551n, 'Ondo Testnet'], + [16098325658947243212n, 'Pharos Atlantic Testnet', '0x1E202D00714bFBcD7a5b4CF782791C38DA8BdC99'], + [4012524741200567430n, 'Pharos Testnet'], + [3967220077692964309n, 'Plasma Testnet', '0xEC7088f7952ba58f268E25AC3868DF92bF462AEf'], + [3743020999916460931n, 'Plume Devnet'], + [14684575664602284776n, 'Plume Testnet'], + [13874588925447303949n, 'Plume Testnet', '0x5e5Fd4720E1CE826138D043aF578D69f48af502F'], + [6955638871347136141n, 'Astar Shibuya', '0x22aE550d87eBf775E0c1fDc8881121c8A51F5903'], + [2333097300889804761n, 'Polkadot Testnet Centrifuge Altair'], + [4340886533089894000n, 'Polkadot Testnet Darwinia Pangoro'], + [5361632739113536121n, 'Polkadot Testnet Moonbeam Moonbase'], + [16281711391670634445n, 'Polygon Amoy', '0x9C32fCB86BF0f4a1A8921a9Fe46de3198bb884B2'], + [12532609583862916517n, 'Polygon Testnet Mumbai'], + [9090863410735740267n, 'Katana Tatara', '0x1dF1fe714A376f248d51AAB826C3feeC379e80fC'], + [6915682381028791124n, 'Private Testnet Andesite'], + [3260900564719373474n, 'Private Testnet Granite'], + [4489326297382772450n, 'Private Testnet Mica'], + [6260932437388305511n, 'Private Testnet Obsidian'], + [8446413392851542429n, 'Private Testnet Opala'], + [13116810400804392105n, 'Ronin Saigon', '0x0aCAe4e51D3DA12Dd3F45A66e8b660f740e6b820'], + [1216300075444106652n, 'Sei Testnet', '0x59F5222c5d77f8D3F56e34Ff7E75A05d2cF3a98A'], + [17833296867764334567n, 'Shibarium Puppynet', '0x449E234FEDF3F907b9E9Dd6BAf1ddc36664097E5'], + [3676871237479449268n, 'Sonic Blaze', '0x2fBd4659774D468Db5ca5bacE37869905d8EfA34'], + [11793402411494852765n, 'Stable Testnet'], + [4237030917318060427n, 'Story Testnet'], + [13694007683517087973n, 'Superseed Sepolia', '0xC3388E1147C5F049db9dd254Dbfa06ab7F19e7FE'], + [9488606126177218005n, 'TAC Saint Petersburg', '0x1D0b2edF6b66845872b6cC82C036E3601Cb2Be57'], + [729797994450396300n, 'Telos EVM Testnet'], + [3963528237232804922n, 'Tempo', '0xAE7D1b3D8466718378038de45D4D376E73A04EB6'], + [3676916124122457866n, 'Treasure Topaz', '0x7425448a70fEb77F0319cC8cD19691FECE7F5C05'], + [13231703482326770600n, 'Tron Devnet EVM'], + [2052925811360307749n, 'Tron Testnet Nile EVM'], + [13231703482326770598n, 'Tron Testnet Shasta EVM'], + [572210378683744374n, 'Velas Testnet'], + [9284632837123596123n, 'Wemix Testnet', '0xA8C0c11bf64AF62CDCA6f93D3769B88BdD7cb93D'], + [3017758115101368649n, 'XDC Apothem Network', '0x1D0b2edF6b66845872b6cC82C036E3601Cb2Be57'], + [10212741611335999305n, 'Xlayer Testnet'], + [2285225387454015855n, 'Zero G Testnet Galileo'], + [13781831279385219069n, 'Zircuit Testnet Garfield'], + [5837261596322416298n, 'Zklink_nova Testnet'], + [16244020411108056671n, 'Zora Sepolia', '0xC5c058814cb85bF52c83264e09da90CB4c932cb7'], +] + +for (const [chainSelector, displayName, router] of chains) { + registerDeployment({ chainSelector, displayName, router }) +} diff --git a/ccip-config/src/chains/index.ts b/ccip-config/src/chains/index.ts new file mode 100644 index 00000000..757e645e --- /dev/null +++ b/ccip-config/src/chains/index.ts @@ -0,0 +1,6 @@ +// Side-effect imports to register all chain deployments +import './evm/index.ts' +import './solana/index.ts' +import './aptos/index.ts' +import './sui/index.ts' +import './ton/index.ts' diff --git a/ccip-config/src/chains/solana/index.ts b/ccip-config/src/chains/solana/index.ts new file mode 100644 index 00000000..a43f95f7 --- /dev/null +++ b/ccip-config/src/chains/solana/index.ts @@ -0,0 +1,3 @@ +// Side-effect imports to register Solana chain deployments +import './mainnet.ts' +import './testnet.ts' diff --git a/ccip-config/src/chains/solana/mainnet.ts b/ccip-config/src/chains/solana/mainnet.ts new file mode 100644 index 00000000..787117ef --- /dev/null +++ b/ccip-config/src/chains/solana/mainnet.ts @@ -0,0 +1,14 @@ +import { registerDeployment } from '../../registry.ts' + +// CCIP Solana Mainnet deployments +// Only contains: chainSelector, displayName, router +// Protocol data (chainId, name, family, isTestnet) lives in the SDK + +// [chainSelector, displayName, router?] +export const chains: readonly [bigint, string, string?][] = [ + [124615329519749607n, 'Solana', 'Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C'], +] + +for (const [chainSelector, displayName, router] of chains) { + registerDeployment({ chainSelector, displayName, router }) +} diff --git a/ccip-config/src/chains/solana/testnet.ts b/ccip-config/src/chains/solana/testnet.ts new file mode 100644 index 00000000..bcf81fb1 --- /dev/null +++ b/ccip-config/src/chains/solana/testnet.ts @@ -0,0 +1,15 @@ +import { registerDeployment } from '../../registry.ts' + +// CCIP Solana Testnet/Devnet deployments +// Only contains: chainSelector, displayName, router +// Protocol data (chainId, name, family, isTestnet) lives in the SDK + +// [chainSelector, displayName, router?] +export const chains: readonly [bigint, string, string?][] = [ + [16423721717087811551n, 'Solana Devnet', 'Ccip842gzYHhvdDkSyi2YVCoAWPbYJoApMFzSxQroE9C'], + [6302590918974934319n, 'Solana Testnet'], +] + +for (const [chainSelector, displayName, router] of chains) { + registerDeployment({ chainSelector, displayName, router }) +} diff --git a/ccip-config/src/chains/sui/index.ts b/ccip-config/src/chains/sui/index.ts new file mode 100644 index 00000000..2e49a74a --- /dev/null +++ b/ccip-config/src/chains/sui/index.ts @@ -0,0 +1,3 @@ +// Side-effect imports to register Sui chain deployments +import './mainnet.ts' +import './testnet.ts' diff --git a/ccip-config/src/chains/sui/mainnet.ts b/ccip-config/src/chains/sui/mainnet.ts new file mode 100644 index 00000000..40b54263 --- /dev/null +++ b/ccip-config/src/chains/sui/mainnet.ts @@ -0,0 +1,12 @@ +import { registerDeployment } from '../../registry.ts' + +// CCIP Sui Mainnet deployments +// Only contains: chainSelector, displayName, router +// Protocol data (chainId, name, family, isTestnet) lives in the SDK + +// [chainSelector, displayName, router?] +export const chains: readonly [bigint, string, string?][] = [[17529533435026248318n, 'Sui']] + +for (const [chainSelector, displayName, router] of chains) { + registerDeployment({ chainSelector, displayName, router }) +} diff --git a/ccip-config/src/chains/sui/testnet.ts b/ccip-config/src/chains/sui/testnet.ts new file mode 100644 index 00000000..ccb8fc75 --- /dev/null +++ b/ccip-config/src/chains/sui/testnet.ts @@ -0,0 +1,15 @@ +import { registerDeployment } from '../../registry.ts' + +// CCIP Sui Testnet/Localnet deployments +// Only contains: chainSelector, displayName, router +// Protocol data (chainId, name, family, isTestnet) lives in the SDK + +// [chainSelector, displayName, router?] +export const chains: readonly [bigint, string, string?][] = [ + [18395503381733958356n, 'Sui Localnet'], + [9762610643973837292n, 'Sui Testnet'], +] + +for (const [chainSelector, displayName, router] of chains) { + registerDeployment({ chainSelector, displayName, router }) +} diff --git a/ccip-config/src/chains/ton/index.ts b/ccip-config/src/chains/ton/index.ts new file mode 100644 index 00000000..32ee178e --- /dev/null +++ b/ccip-config/src/chains/ton/index.ts @@ -0,0 +1,3 @@ +// Side-effect imports to register TON chain deployments +import './mainnet.ts' +import './testnet.ts' diff --git a/ccip-config/src/chains/ton/mainnet.ts b/ccip-config/src/chains/ton/mainnet.ts new file mode 100644 index 00000000..7a294601 --- /dev/null +++ b/ccip-config/src/chains/ton/mainnet.ts @@ -0,0 +1,12 @@ +import { registerDeployment } from '../../registry.ts' + +// CCIP TON Mainnet deployments +// Only contains: chainSelector, displayName, router +// Protocol data (chainId, name, family, isTestnet) lives in the SDK + +// [chainSelector, displayName, router?] +export const chains: readonly [bigint, string, string?][] = [[16448340667252469081n, 'TON']] + +for (const [chainSelector, displayName, router] of chains) { + registerDeployment({ chainSelector, displayName, router }) +} diff --git a/ccip-config/src/chains/ton/testnet.ts b/ccip-config/src/chains/ton/testnet.ts new file mode 100644 index 00000000..aa9422bc --- /dev/null +++ b/ccip-config/src/chains/ton/testnet.ts @@ -0,0 +1,15 @@ +import { registerDeployment } from '../../registry.ts' + +// CCIP TON Testnet/Localnet deployments +// Only contains: chainSelector, displayName, router +// Protocol data (chainId, name, family, isTestnet) lives in the SDK + +// [chainSelector, displayName, router?] +export const chains: readonly [bigint, string, string?][] = [ + [13879075125137744094n, 'TON Localnet'], + [1399300952838017768n, 'TON Testnet'], +] + +for (const [chainSelector, displayName, router] of chains) { + registerDeployment({ chainSelector, displayName, router }) +} diff --git a/ccip-config/src/errors.ts b/ccip-config/src/errors.ts new file mode 100644 index 00000000..8791dd7a --- /dev/null +++ b/ccip-config/src/errors.ts @@ -0,0 +1,112 @@ +/** + * Error codes for programmatic error handling. + */ +export const ErrorCodes = { + DEPLOYMENT_NOT_FOUND: 'CCIP_DEPLOYMENT_NOT_FOUND', + ROUTER_NOT_FOUND: 'CCIP_ROUTER_NOT_FOUND', + VALIDATION_ERROR: 'CCIP_VALIDATION_ERROR', +} as const + +/** Error code type for programmatic handling. */ +export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes] + +/** + * Base error for ccip-config errors. + */ +export class CCIPConfigError extends Error { + readonly code: ErrorCode + readonly recovery?: string + + /** + * Create a new CCIPConfigError. + * @param code - Error code for programmatic handling + * @param message - Error message + * @param recovery - Recovery suggestion + */ + constructor(code: ErrorCode, message: string, recovery?: string) { + super(message) + this.name = 'CCIPConfigError' + this.code = code + this.recovery = recovery + } +} + +/** + * Thrown when deployment data is not found for a chain selector. + */ +export class CCIPDeploymentNotFoundError extends CCIPConfigError { + readonly chainSelector: bigint + + /** + * Create a new CCIPDeploymentNotFoundError. + * @param chainSelector - The chain selector that was not found + */ + constructor(chainSelector: bigint) { + super( + ErrorCodes.DEPLOYMENT_NOT_FOUND, + `No deployment found for chain selector ${chainSelector}`, + 'Check if chain data is imported. Use: import "@chainlink/ccip-config/chains/evm/mainnet"', + ) + this.name = 'CCIPDeploymentNotFoundError' + this.chainSelector = chainSelector + } +} + +/** + * Thrown when deployment data is not found for a display name. + */ +export class CCIPDeploymentNotFoundByNameError extends CCIPConfigError { + readonly displayName: string + + /** + * Create a new CCIPDeploymentNotFoundByNameError. + * @param displayName - The display name that was not found + */ + constructor(displayName: string) { + super( + ErrorCodes.DEPLOYMENT_NOT_FOUND, + `No deployment found for chain name "${displayName}"`, + 'Check if chain data is imported. Use: import "@chainlink/ccip-config/chains/evm/mainnet"', + ) + this.name = 'CCIPDeploymentNotFoundByNameError' + this.displayName = displayName + } +} + +/** + * Thrown when a chain exists but has no CCIP router deployed. + */ +export class CCIPRouterNotFoundError extends CCIPConfigError { + readonly chainSelector: bigint + readonly displayName: string + + /** + * Create a new CCIPRouterNotFoundError. + * @param chainSelector - The chain selector + * @param displayName - The chain's display name + */ + constructor(chainSelector: bigint, displayName: string) { + super( + ErrorCodes.ROUTER_NOT_FOUND, + `No router configured for ${displayName} (${chainSelector})`, + 'This chain may not have CCIP deployed yet, or use a custom router address.', + ) + this.name = 'CCIPRouterNotFoundError' + this.chainSelector = chainSelector + this.displayName = displayName + } +} + +/** + * Thrown when deployment data fails validation. + */ +export class CCIPValidationError extends CCIPConfigError { + /** + * Create a new CCIPValidationError. + * @param message - Error message describing the validation failure + */ + constructor(message: string) { + super(ErrorCodes.VALIDATION_ERROR, message) + this.name = 'CCIPValidationError' + } +} diff --git a/ccip-config/src/index.test.ts b/ccip-config/src/index.test.ts new file mode 100644 index 00000000..5a91e16c --- /dev/null +++ b/ccip-config/src/index.test.ts @@ -0,0 +1,723 @@ +import assert from 'node:assert' +import { before, describe, it } from 'node:test' + +import { decodeAddress, networkInfo } from '@chainlink/ccip-sdk/src/index.ts' + +import { + CCIPDeploymentNotFoundByNameError, + CCIPDeploymentNotFoundError, + CCIPRouterNotFoundError, + CCIPValidationError, +} from './errors.ts' +import { + getDeploymentByName, + getDisplayName, + getRouter, + getRouterByName, + isCCIPEnabled, + isCCIPEnabledBySelector, + requireDeployment, + requireDeploymentByName, + requireRouter, + requireRouterByName, +} from './lookup.ts' +import { + createRegistry, + getAllDeployments, + getDeployment, + resetLogger, + setLogger, +} from './registry.ts' +import type { ChainDeployment } from './types.ts' + +// Import chains to register them +import './chains/evm/mainnet.ts' +import './chains/evm/testnet.ts' +import './chains/solana/index.ts' +import './chains/aptos/index.ts' +import './chains/sui/index.ts' +import './chains/ton/index.ts' + +// Known selectors for testing +const ETHEREUM_MAINNET_SELECTOR = 5009297550715157269n + +// Silent logger for tests (matches Logger interface from ccip-sdk) +const silentLogger = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +} + +// Snapshot of real deployments BEFORE any test fixtures are added +// This is used for validation tests to avoid test pollution +let realDeployments: readonly ChainDeployment[] +before(() => { + realDeployments = getAllDeployments() +}) + +describe('ccip-config', () => { + describe('getDeployment', () => { + it('returns undefined for unknown selector', () => { + const result = getDeployment(999n) + assert.strictEqual(result, undefined) + }) + + it('finds deployment by selector', () => { + const result = getDeployment(ETHEREUM_MAINNET_SELECTOR) + assert.ok(result) + assert.strictEqual(result.displayName, 'Ethereum') + assert.strictEqual(result.router, '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D') + }) + }) + + describe('requireDeployment', () => { + it('throws CCIPDeploymentNotFoundError for unknown selector', () => { + assert.throws(() => requireDeployment(999n), CCIPDeploymentNotFoundError) + }) + + it('returns deployment for known selector', () => { + const result = requireDeployment(ETHEREUM_MAINNET_SELECTOR) + assert.strictEqual(result.displayName, 'Ethereum') + }) + }) + + describe('getRouter', () => { + it('returns router for chain with router', () => { + const router = getRouter(ETHEREUM_MAINNET_SELECTOR) + assert.strictEqual(router, '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D') + }) + + it('returns undefined for unknown selector', () => { + const router = getRouter(999n) + assert.strictEqual(router, undefined) + }) + }) + + describe('requireRouter', () => { + it('returns router for chain with router', () => { + const router = requireRouter(ETHEREUM_MAINNET_SELECTOR) + assert.strictEqual(router, '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D') + }) + + it('throws CCIPDeploymentNotFoundError for unknown selector', () => { + assert.throws(() => requireRouter(999n), CCIPDeploymentNotFoundError) + }) + + it('throws CCIPRouterNotFoundError for chain without router', () => { + // Use isolated registry with skipValidation for test data + const registry = createRegistry({ skipValidation: true }) + registry.register({ + chainSelector: 999999999999n, + displayName: 'Test No Router', + }) + + // Test that requireRouter works correctly with deployment without router + const deployment = registry.get(999999999999n) + assert.ok(deployment) + assert.strictEqual(deployment.router, undefined) + // The global requireRouter won't find this (it's in isolated registry) + // so we verify the error by checking the CCIPRouterNotFoundError class exists + const error = new CCIPRouterNotFoundError(999999999999n, 'Test No Router') + assert.strictEqual(error.name, 'CCIPRouterNotFoundError') + assert.ok(error.message.includes('Test No Router')) + }) + }) + + describe('getDisplayName', () => { + it('returns display name for known chain', () => { + const name = getDisplayName(ETHEREUM_MAINNET_SELECTOR) + assert.strictEqual(name, 'Ethereum') + }) + + it('returns undefined for unknown selector', () => { + const name = getDisplayName(999n) + assert.strictEqual(name, undefined) + }) + }) + + describe('isCCIPEnabled (type guard)', () => { + it('returns true and narrows type for deployment with router', () => { + const deployment = getDeployment(ETHEREUM_MAINNET_SELECTOR) + assert.ok(deployment) + if (isCCIPEnabled(deployment)) { + // TypeScript should narrow to CCIPEnabledDeployment here + const router: string = deployment.router + assert.strictEqual(router, '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D') + } else { + assert.fail('Expected isCCIPEnabled to return true') + } + }) + + it('returns false for deployment without router', () => { + // Create a deployment object directly (no need to register) + // isCCIPEnabled is a pure function that just checks the router property + const deployment: ChainDeployment = { + chainSelector: 888888888888n, + name: 'test-no-router', + displayName: 'Test No Router Type Guard', + } + assert.strictEqual(isCCIPEnabled(deployment), false) + }) + }) + + describe('isCCIPEnabledBySelector', () => { + it('returns true for chain with router', () => { + assert.strictEqual(isCCIPEnabledBySelector(ETHEREUM_MAINNET_SELECTOR), true) + }) + + it('returns false for unknown selector', () => { + assert.strictEqual(isCCIPEnabledBySelector(999n), false) + }) + }) + + describe('getAllDeployments', () => { + it('returns all registered deployments', () => { + const deployments = getAllDeployments() + // Should have 100+ chains from EVM mainnet + testnet + Solana + Aptos + assert.ok(deployments.length > 100) + }) + }) + + describe('registerDeployment', () => { + it('registers a deployment', () => { + // Use isolated registry with skipValidation to avoid polluting global state + const registry = createRegistry({ skipValidation: true }) + registry.register({ + chainSelector: 123456789n, + displayName: 'My Custom Chain', + router: '0x1234567890abcdef', + }) + + const result = registry.get(123456789n) + assert.ok(result) + assert.strictEqual(result.displayName, 'My Custom Chain') + assert.strictEqual(result.router, '0x1234567890abcdef') + }) + }) + + describe('getDeploymentByName', () => { + it('finds deployment by SDK canonical name', () => { + // SDK canonical name is 'ethereum-mainnet', not 'Ethereum' (display name) + const deployment = getDeploymentByName('ethereum-mainnet') + assert.ok(deployment) + assert.strictEqual(deployment.chainSelector, ETHEREUM_MAINNET_SELECTOR) + assert.strictEqual(deployment.name, 'ethereum-mainnet') + assert.strictEqual(deployment.displayName, 'Ethereum') + }) + + it('returns undefined for unknown name', () => { + const deployment = getDeploymentByName('Unknown Chain XYZ') + assert.strictEqual(deployment, undefined) + }) + + it('is case-sensitive (uses SDK canonical name)', () => { + // SDK canonical names are lowercase + const lowercase = getDeploymentByName('ethereum-mainnet') + const uppercase = getDeploymentByName('Ethereum-Mainnet') + assert.ok(lowercase) + assert.strictEqual(uppercase, undefined) // Case-sensitive, so not found + }) + }) + + describe('getRouterByName', () => { + it('returns router for known chain with router', () => { + const router = getRouterByName('ethereum-mainnet') + assert.strictEqual(router, '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D') + }) + + it('returns undefined for unknown name', () => { + const router = getRouterByName('Unknown Chain XYZ') + assert.strictEqual(router, undefined) + }) + + it('is case-sensitive (uses SDK canonical name)', () => { + const lowercase = getRouterByName('ethereum-mainnet') + const uppercase = getRouterByName('ETHEREUM-MAINNET') + assert.strictEqual(lowercase, '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D') + assert.strictEqual(uppercase, undefined) // Case-sensitive, so not found + }) + }) + + describe('requireDeploymentByName', () => { + it('returns deployment for known name', () => { + const deployment = requireDeploymentByName('ethereum-mainnet') + assert.strictEqual(deployment.chainSelector, ETHEREUM_MAINNET_SELECTOR) + assert.strictEqual(deployment.name, 'ethereum-mainnet') + assert.strictEqual(deployment.displayName, 'Ethereum') + }) + + it('throws CCIPDeploymentNotFoundByNameError for unknown name', () => { + assert.throws( + () => requireDeploymentByName('Unknown Chain XYZ'), + CCIPDeploymentNotFoundByNameError, + ) + }) + + it('is case-sensitive (uses SDK canonical name)', () => { + // SDK canonical names are lowercase + const deployment = requireDeploymentByName('ethereum-mainnet') + assert.strictEqual(deployment.displayName, 'Ethereum') + // Uppercase should throw + assert.throws( + () => requireDeploymentByName('ETHEREUM-MAINNET'), + CCIPDeploymentNotFoundByNameError, + ) + }) + }) + + describe('requireRouterByName', () => { + it('returns router for known chain with router', () => { + const router = requireRouterByName('ethereum-mainnet') + assert.strictEqual(router, '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D') + }) + + it('throws CCIPDeploymentNotFoundByNameError for unknown name', () => { + assert.throws( + () => requireRouterByName('Unknown Chain XYZ'), + CCIPDeploymentNotFoundByNameError, + ) + }) + + it('throws CCIPRouterNotFoundError for chain without router', () => { + // Create a deployment without router to test the error + // We can't add to global registry, but we can test the error class directly + const error = new CCIPRouterNotFoundError(123n, 'Test Chain') + assert.strictEqual(error.name, 'CCIPRouterNotFoundError') + assert.ok(error.message.includes('Test Chain')) + assert.strictEqual(error.chainSelector, 123n) + }) + + it('is case-sensitive (uses SDK canonical name)', () => { + const router = requireRouterByName('ethereum-mainnet') + assert.strictEqual(router, '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D') + // Uppercase should throw + assert.throws( + () => requireRouterByName('ETHEREUM-MAINNET'), + CCIPDeploymentNotFoundByNameError, + ) + }) + }) + + describe('CCIPValidationError', () => { + it('throws for invalid chainSelector (zero)', () => { + const registry = createRegistry({ logger: silentLogger }) + assert.throws( + () => registry.register({ chainSelector: 0n, displayName: 'Test' }), + CCIPValidationError, + ) + }) + + it('throws for invalid chainSelector (negative)', () => { + const registry = createRegistry({ logger: silentLogger }) + assert.throws( + () => registry.register({ chainSelector: -1n, displayName: 'Test' }), + CCIPValidationError, + ) + }) + + it('throws for empty displayName', () => { + const registry = createRegistry({ logger: silentLogger }) + assert.throws( + () => registry.register({ chainSelector: 1n, displayName: '' }), + CCIPValidationError, + ) + }) + + it('throws for whitespace-only displayName', () => { + const registry = createRegistry({ logger: silentLogger }) + assert.throws( + () => registry.register({ chainSelector: 1n, displayName: ' ' }), + CCIPValidationError, + ) + }) + + it('throws for empty router string', () => { + const registry = createRegistry({ logger: silentLogger }) + assert.throws( + () => registry.register({ chainSelector: 1n, displayName: 'Test', router: '' }), + CCIPValidationError, + ) + }) + + it('has correct error properties', () => { + const error = new CCIPValidationError('Test validation message') + assert.strictEqual(error.name, 'CCIPValidationError') + assert.strictEqual(error.code, 'CCIP_VALIDATION_ERROR') + assert.ok(error.message.includes('Test validation message')) + }) + }) + + describe('createRegistry (isolated registry)', () => { + it('creates an isolated registry that does not affect global registry', () => { + const registry = createRegistry({ skipValidation: true }) + + // Register in isolated registry + registry.register({ + chainSelector: 777777777777n, + displayName: 'Isolated Test Chain', + router: '0xIsolatedRouter', + }) + + // Should be found in isolated registry + const isolatedResult = registry.get(777777777777n) + assert.ok(isolatedResult) + assert.strictEqual(isolatedResult.displayName, 'Isolated Test Chain') + + // Should NOT be found in global registry + const globalResult = getDeployment(777777777777n) + assert.strictEqual(globalResult, undefined) + }) + + it('multiple isolated registries are independent', () => { + const registry1 = createRegistry({ skipValidation: true }) + const registry2 = createRegistry({ skipValidation: true }) + + registry1.register({ + chainSelector: 111n, + displayName: 'Registry 1 Chain', + }) + + registry2.register({ + chainSelector: 222n, + displayName: 'Registry 2 Chain', + }) + + // Each registry only has its own chains + assert.ok(registry1.get(111n)) + assert.strictEqual(registry1.get(222n), undefined) + assert.ok(registry2.get(222n)) + assert.strictEqual(registry2.get(111n), undefined) + }) + + it('getCCIPEnabled returns only CCIP-enabled deployments', () => { + const registry = createRegistry({ skipValidation: true }) + + registry.register({ + chainSelector: 1n, + displayName: 'With Router', + router: '0xRouter', + }) + + registry.register({ + chainSelector: 2n, + displayName: 'Without Router', + }) + + const enabled = registry.getCCIPEnabled() + assert.strictEqual(enabled.length, 1) + assert.strictEqual(enabled[0]?.displayName, 'With Router') + }) + + it('getCCIPEnabledCount returns correct count', () => { + const registry = createRegistry({ skipValidation: true }) + + registry.register({ + chainSelector: 1n, + displayName: 'Chain 1', + router: '0xRouter1', + }) + + registry.register({ + chainSelector: 2n, + displayName: 'Chain 2', + router: '0xRouter2', + }) + + registry.register({ + chainSelector: 3n, + displayName: 'Chain 3 (no router)', + }) + + assert.strictEqual(registry.getCCIPEnabledCount(), 2) + }) + + it('clear removes all deployments', () => { + const registry = createRegistry({ skipValidation: true }) + + registry.register({ + chainSelector: 1n, + displayName: 'Test', + }) + + assert.strictEqual(registry.getAll().length, 1) + registry.clear() + assert.strictEqual(registry.getAll().length, 0) + }) + + it('getByName provides O(1) lookup by SDK canonical name', () => { + // For real registrations (without skipValidation), name comes from SDK + // Use a real chain selector to test + const registry = createRegistry() + + registry.register({ + chainSelector: ETHEREUM_MAINNET_SELECTOR, + displayName: 'Ethereum', + router: '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D', + }) + + // Name should be the SDK canonical name (case-sensitive) + const deployment = registry.getByName('ethereum-mainnet') + assert.ok(deployment) + assert.strictEqual(deployment.name, 'ethereum-mainnet') + assert.strictEqual(deployment.displayName, 'Ethereum') + + // Case-sensitive: uppercase should not match + assert.strictEqual(registry.getByName('ETHEREUM-MAINNET'), undefined) + assert.strictEqual(registry.getByName('Ethereum'), undefined) // Display name, not SDK name + }) + + it('getAll returns frozen array that cannot be mutated', () => { + const registry = createRegistry({ skipValidation: true }) + + registry.register({ + chainSelector: 1n, + displayName: 'Test', + }) + + const all = registry.getAll() + assert.strictEqual(all.length, 1) + + // Attempt to mutate should throw in strict mode or be silently ignored + assert.throws(() => { + ;(all as ChainDeployment[]).push({ + chainSelector: 999n, + name: 'hacked', + displayName: 'Hacked', + }) + }) + + // Original should be unchanged + assert.strictEqual(registry.getAll().length, 1) + }) + + it('updates name index on duplicate registration', () => { + const registry = createRegistry({ skipValidation: true }) + + // Suppress warning for this test + setLogger(silentLogger) + + registry.register({ + chainSelector: 1n, + displayName: 'Original Name', + }) + + // When skipValidation is true, name is generated as 'test-chain-${chainSelector}' + const originalName = 'test-chain-1' + + // Re-register same selector with different displayName (name stays the same in skipValidation mode) + registry.register({ + chainSelector: 1n, + displayName: 'Updated Name', + }) + + // Name should still be found (it's auto-generated in skipValidation mode) + const updated = registry.getByName(originalName) + assert.ok(updated) + assert.strictEqual(updated.chainSelector, 1n) + assert.strictEqual(updated.displayName, 'Updated Name') + + resetLogger() + }) + }) + + describe('setLogger (injectable logger)', () => { + it('allows custom logger for duplicate registration warnings', () => { + const warnings: string[] = [] + const customLogger = { + debug: () => {}, + info: () => {}, + warn: (...args: unknown[]) => warnings.push(String(args[0])), + error: () => {}, + } + + setLogger(customLogger) + + const registry = createRegistry({ skipValidation: true }) + registry.register({ + chainSelector: 1n, + displayName: 'First', + }) + registry.register({ + chainSelector: 1n, + displayName: 'Second', + }) + + // Should have captured the warning + assert.strictEqual(warnings.length, 1) + assert.ok(warnings[0]?.includes('Duplicate registration')) + + resetLogger() + }) + + it('allows silent mode by providing no-op logger', () => { + setLogger(silentLogger) + + const registry = createRegistry({ skipValidation: true }) + // This should not throw or log anything + registry.register({ + chainSelector: 1n, + displayName: 'First', + }) + registry.register({ + chainSelector: 1n, + displayName: 'Second', + }) + + // Just verify we can register without issues + const deployment = registry.get(1n) + assert.strictEqual(deployment?.displayName, 'Second') + + resetLogger() + }) + + it('per-registry logger overrides global logger', () => { + const globalWarnings: string[] = [] + const registryWarnings: string[] = [] + + const globalLogger = { + debug: () => {}, + info: () => {}, + warn: (...args: unknown[]) => globalWarnings.push(String(args[0])), + error: () => {}, + } + + const registryLogger = { + debug: () => {}, + info: () => {}, + warn: (...args: unknown[]) => registryWarnings.push(String(args[0])), + error: () => {}, + } + + setLogger(globalLogger) + + // Registry with its own logger (also needs skipValidation for test data) + const registry = createRegistry({ logger: registryLogger, skipValidation: true }) + registry.register({ chainSelector: 1n, displayName: 'First' }) + registry.register({ chainSelector: 1n, displayName: 'Second' }) + + // Should use per-registry logger, not global + assert.strictEqual(globalWarnings.length, 0) + assert.strictEqual(registryWarnings.length, 1) + assert.ok(registryWarnings[0]?.includes('Duplicate registration')) + + resetLogger() + }) + + it('warns on SDK canonical name collision (different selector, same name)', () => { + const warnings: string[] = [] + const customLogger = { + debug: () => {}, + info: () => {}, + warn: (...args: unknown[]) => warnings.push(String(args[0])), + error: () => {}, + } + + // In skipValidation mode, name is auto-generated as 'test-chain-${chainSelector}' + // so we can't easily test name collision. Instead, test that the warning message + // format is correct when it would occur + const registry = createRegistry({ logger: customLogger, skipValidation: true }) + registry.register({ chainSelector: 1n, displayName: 'First' }) + registry.register({ chainSelector: 2n, displayName: 'Second' }) + + // No warnings expected (different selectors, different auto-generated names) + assert.strictEqual(warnings.length, 0) + }) + + it('does not warn when re-registering same selector with same name', () => { + const warnings: string[] = [] + const customLogger = { + debug: () => {}, + info: () => {}, + warn: (...args: unknown[]) => warnings.push(String(args[0])), + error: () => {}, + } + + const registry = createRegistry({ logger: customLogger, skipValidation: true }) + registry.register({ chainSelector: 1n, displayName: 'Same Name' }) + registry.register({ chainSelector: 1n, displayName: 'Same Name' }) + + // Should warn about duplicate registration, but NOT name collision + assert.strictEqual(warnings.length, 1) + assert.ok(warnings[0]?.includes('Duplicate registration')) + assert.ok(!warnings[0]?.includes('Name collision')) + }) + }) + + /** + * CRITICAL VALIDATION TEST + * + * This test ensures that ALL chain selectors registered in ccip-config + * are valid and known to ccip-sdk's networkInfo(). + * + * This prevents: + * - Typos in chainSelector values + * - Registering chains that don't exist in the SDK + * - Deployment/protocol data mismatch + * + * If this test fails, it means a deployment was registered with a + * chainSelector that the SDK doesn't recognize. + * + * NOTE: Uses `realDeployments` snapshot taken before test fixtures are added. + */ + describe('deployment validation against SDK', () => { + it('all registered chainSelectors must exist in ccip-sdk', () => { + const errors: string[] = [] + + for (const deployment of realDeployments) { + try { + // This will throw if the chainSelector is unknown to the SDK + const network = networkInfo(deployment.chainSelector) + + // Additional validation: family should match if we can infer it + // (networkInfo returns the canonical chain info from SDK) + assert.ok( + network.chainSelector === deployment.chainSelector, + `Selector mismatch for ${deployment.displayName}`, + ) + } catch (_err) { + errors.push( + `${deployment.displayName} (selector: ${deployment.chainSelector}) - ` + + `not found in ccip-sdk. Did you forget to add it to selectors.ts?`, + ) + } + } + + if (errors.length > 0) { + assert.fail( + `Found ${errors.length} deployment(s) with unknown chainSelectors:\n\n` + + errors.map((e) => ` ❌ ${e}`).join('\n') + + '\n\nFix: Add the missing chains to ccip-sdk/src/selectors.ts first, ' + + 'then register the deployment in ccip-config.', + ) + } + }) + + it('all registered deployments should have valid displayName', () => { + for (const deployment of realDeployments) { + assert.ok( + deployment.displayName && deployment.displayName.trim().length > 0, + `Deployment ${deployment.chainSelector} has empty displayName`, + ) + } + }) + + it('all router addresses should be valid for their chain family', () => { + for (const deployment of realDeployments) { + if (deployment.router) { + // Use SDK's address validation which is family-aware + const network = networkInfo(deployment.chainSelector) + try { + // decodeAddress throws if the address is invalid for the family + decodeAddress(deployment.router, network.family) + } catch (err) { + assert.fail( + `${deployment.displayName} (${network.family}) has invalid router: ${deployment.router}\n` + + `Error: ${err instanceof Error ? err.message : String(err)}`, + ) + } + } + } + }) + }) +}) diff --git a/ccip-config/src/index.ts b/ccip-config/src/index.ts new file mode 100644 index 00000000..b475c374 --- /dev/null +++ b/ccip-config/src/index.ts @@ -0,0 +1,40 @@ +// Types +export type { CCIPEnabledDeployment, ChainDeployment, ChainDeploymentInput } from './types.ts' +export type { Logger, Registry, RegistryOptions } from './registry.ts' +export type { ErrorCode } from './errors.ts' + +// Errors +export { + CCIPConfigError, + CCIPDeploymentNotFoundByNameError, + CCIPDeploymentNotFoundError, + CCIPRouterNotFoundError, + CCIPValidationError, + ErrorCodes, +} from './errors.ts' + +// Registry (for advanced use) +export { + clearRegistry, + createRegistry, + getAllDeployments, + getCCIPEnabledCount, + getCCIPEnabledDeployments, + getDeployment, + registerDeployment, + setLogger, +} from './registry.ts' + +// Lookup functions (main API) +export { + getDeploymentByName, + getDisplayName, + getRouter, + getRouterByName, + isCCIPEnabled, + isCCIPEnabledBySelector, + requireDeployment, + requireDeploymentByName, + requireRouter, + requireRouterByName, +} from './lookup.ts' diff --git a/ccip-config/src/lookup.ts b/ccip-config/src/lookup.ts new file mode 100644 index 00000000..101d404b --- /dev/null +++ b/ccip-config/src/lookup.ts @@ -0,0 +1,207 @@ +import { + CCIPDeploymentNotFoundByNameError, + CCIPDeploymentNotFoundError, + CCIPRouterNotFoundError, +} from './errors.ts' +import { getDeployment, getDeploymentByNameFromRegistry } from './registry.ts' +import type { CCIPEnabledDeployment, ChainDeployment } from './types.ts' + +/** + * Get deployment by chain selector, throw if not found. + * + * @param chainSelector - CCIP chain selector + * @returns ChainDeployment + * @throws CCIPDeploymentNotFoundError if not found + */ +export function requireDeployment(chainSelector: bigint): ChainDeployment { + const deployment = getDeployment(chainSelector) + if (!deployment) { + throw new CCIPDeploymentNotFoundError(chainSelector) + } + return deployment +} + +/** + * Get router address for a chain. + * + * @param chainSelector - CCIP chain selector + * @returns Router address if found and configured, undefined otherwise + * + * @example + * ```typescript + * import '@chainlink/ccip-config/chains/evm/mainnet' + * import { getRouter } from '@chainlink/ccip-config' + * + * const router = getRouter(5009297550715157269n) + * // '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D' + * ``` + */ +export function getRouter(chainSelector: bigint): string | undefined { + return getDeployment(chainSelector)?.router +} + +/** + * Get router address, throw if not found or not configured. + * + * @param chainSelector - CCIP chain selector + * @returns Router address + * @throws CCIPDeploymentNotFoundError if chain doesn't exist + * @throws CCIPRouterNotFoundError if chain has no router + * + * @example + * ```typescript + * import '@chainlink/ccip-config/chains/evm/mainnet' + * import { requireRouter } from '@chainlink/ccip-config' + * + * const router = requireRouter(5009297550715157269n) + * // '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D' + * ``` + */ +export function requireRouter(chainSelector: bigint): string { + const deployment = requireDeployment(chainSelector) + if (!deployment.router) { + throw new CCIPRouterNotFoundError(chainSelector, deployment.displayName) + } + return deployment.router +} + +/** + * Get display name for a chain. + * + * @param chainSelector - CCIP chain selector + * @returns Display name if found, undefined otherwise + */ +export function getDisplayName(chainSelector: bigint): string | undefined { + return getDeployment(chainSelector)?.displayName +} + +/** + * Type guard to check if a deployment has CCIP router configured. + * Narrows ChainDeployment to CCIPEnabledDeployment. + * + * @param deployment - Chain deployment to check + * @returns true if deployment has router configured + * + * @example + * ```typescript + * import '@chainlink/ccip-config/chains/evm/mainnet' + * import { getDeployment, isCCIPEnabled } from '@chainlink/ccip-config' + * + * const deployment = getDeployment(5009297550715157269n) + * if (deployment && isCCIPEnabled(deployment)) { + * // deployment is narrowed to CCIPEnabledDeployment + * console.log(deployment.router) // string, not string | undefined + * } + * ``` + */ +export function isCCIPEnabled(deployment: ChainDeployment): deployment is CCIPEnabledDeployment { + return deployment.router !== undefined +} + +/** + * Check if a chain has CCIP router configured by selector. + * + * @param chainSelector - CCIP chain selector + * @returns true if chain exists and has router configured + * + * @example + * ```typescript + * import '@chainlink/ccip-config/chains/evm/mainnet' + * import { isCCIPEnabledBySelector } from '@chainlink/ccip-config' + * + * if (isCCIPEnabledBySelector(5009297550715157269n)) { + * // Safe to send CCIP messages + * } + * ``` + */ +export function isCCIPEnabledBySelector(chainSelector: bigint): boolean { + const deployment = getDeployment(chainSelector) + return deployment !== undefined && deployment.router !== undefined +} + +/** + * Find deployment by SDK canonical name (O(1) lookup). + * + * @param name - SDK canonical name (e.g., "ethereum-mainnet") + * @returns Deployment if found, undefined otherwise + * + * @example + * ```typescript + * import '@chainlink/ccip-config/chains/evm/mainnet' + * import { getDeploymentByName } from '@chainlink/ccip-config' + * + * const deployment = getDeploymentByName('ethereum-mainnet') + * // { chainSelector: 5009297550715157269n, name: 'ethereum-mainnet', displayName: 'Ethereum', router: '0x...' } + * ``` + */ +export function getDeploymentByName(name: string): ChainDeployment | undefined { + return getDeploymentByNameFromRegistry(name) +} + +/** + * Get router address by SDK canonical name (O(1) lookup). + * + * @param name - SDK canonical name (e.g., "ethereum-mainnet") + * @returns Router address if found and configured, undefined otherwise + * + * @example + * ```typescript + * import '@chainlink/ccip-config/chains/evm/mainnet' + * import { getRouterByName } from '@chainlink/ccip-config' + * + * const router = getRouterByName('ethereum-mainnet') + * // '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D' + * ``` + */ +export function getRouterByName(name: string): string | undefined { + return getDeploymentByNameFromRegistry(name)?.router +} + +/** + * Get deployment by SDK canonical name, throw if not found (O(1) lookup). + * + * @param name - SDK canonical name (e.g., "ethereum-mainnet") + * @returns ChainDeployment + * @throws CCIPDeploymentNotFoundByNameError if not found + * + * @example + * ```typescript + * import '@chainlink/ccip-config/chains/evm/mainnet' + * import { requireDeploymentByName } from '@chainlink/ccip-config' + * + * const deployment = requireDeploymentByName('ethereum-mainnet') + * // { chainSelector: 5009297550715157269n, name: 'ethereum-mainnet', displayName: 'Ethereum', router: '0x...' } + * ``` + */ +export function requireDeploymentByName(name: string): ChainDeployment { + const deployment = getDeploymentByNameFromRegistry(name) + if (!deployment) { + throw new CCIPDeploymentNotFoundByNameError(name) + } + return deployment +} + +/** + * Get router address by SDK canonical name, throw if not found or not configured. + * + * @param name - SDK canonical name (e.g., "ethereum-mainnet") + * @returns Router address + * @throws CCIPDeploymentNotFoundByNameError if chain doesn't exist + * @throws CCIPRouterNotFoundError if chain has no router + * + * @example + * ```typescript + * import '@chainlink/ccip-config/chains/evm/mainnet' + * import { requireRouterByName } from '@chainlink/ccip-config' + * + * const router = requireRouterByName('ethereum-mainnet') + * // '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D' + * ``` + */ +export function requireRouterByName(name: string): string { + const deployment = requireDeploymentByName(name) + if (!deployment.router) { + throw new CCIPRouterNotFoundError(deployment.chainSelector, deployment.displayName) + } + return deployment.router +} diff --git a/ccip-config/src/registry.ts b/ccip-config/src/registry.ts new file mode 100644 index 00000000..8eba72d9 --- /dev/null +++ b/ccip-config/src/registry.ts @@ -0,0 +1,308 @@ +import { networkInfo } from '@chainlink/ccip-sdk/src/index.ts' + +import { CCIPValidationError } from './errors.ts' +import type { CCIPEnabledDeployment, ChainDeployment, ChainDeploymentInput } from './types.ts' + +/** + * Logger interface for logging messages (compatible with console). + * Matches the Logger interface from ccip-sdk for consistency. + */ +export interface Logger { + debug: (...args: unknown[]) => void + info: (...args: unknown[]) => void + warn: (...args: unknown[]) => void + error: (...args: unknown[]) => void +} + +/** + * Default logger that uses console. + * Can be replaced via setLogger() for custom logging behavior. + */ +const defaultLogger: Logger = console + +let logger: Logger = defaultLogger + +/** + * Set a custom logger for the registry. + * Use this to integrate with your application's logging system or to suppress warnings. + * + * @param customLogger - Logger implementation (compatible with console) + * + * @example + * ```typescript + * import { setLogger } from '@chainlink/ccip-config' + * + * // Use custom logger + * setLogger(myLogger) + * + * // Suppress all logging (silent mode) + * setLogger({ debug: () => {}, info: () => {}, warn: () => {}, error: () => {} }) + * ``` + */ +export function setLogger(customLogger: Logger): void { + logger = customLogger +} + +/** + * Reset logger to default (console). + * Useful for testing cleanup. + * + * @internal + */ +export function resetLogger(): void { + logger = defaultLogger +} + +/** + * Registry interface for chain deployments. + * Provides methods to register, retrieve, and query chain deployment data. + */ +export interface Registry { + /** Register a chain deployment (name is auto-populated from SDK) */ + register(input: ChainDeploymentInput): void + /** Get deployment by chain selector */ + get(chainSelector: bigint): ChainDeployment | undefined + /** Get deployment by SDK canonical name (case-sensitive, O(1) lookup) */ + getByName(name: string): ChainDeployment | undefined + /** Get router address by chain selector */ + getRouter(chainSelector: bigint): string | undefined + /** Get all registered deployments (returns frozen array) */ + getAll(): readonly ChainDeployment[] + /** Get only CCIP-enabled deployments (with router addresses, returns frozen array) */ + getCCIPEnabled(): readonly CCIPEnabledDeployment[] + /** Get count of CCIP-enabled chains (O(1)) */ + getCCIPEnabledCount(): number + /** Clear all deployments */ + clear(): void +} + +/** + * Validate input data and resolve SDK canonical name. + * @returns ChainDeployment with name populated from SDK + * @throws CCIPValidationError if validation fails or chain not in SDK + */ +function validateAndResolve(input: ChainDeploymentInput): ChainDeployment { + if (input.chainSelector <= 0n) { + throw new CCIPValidationError(`Invalid chainSelector: ${input.chainSelector}`) + } + if (!input.displayName || input.displayName.trim() === '') { + throw new CCIPValidationError(`Invalid displayName for chain selector ${input.chainSelector}`) + } + if (input.router !== undefined) { + if (typeof input.router !== 'string' || input.router.length === 0) { + throw new CCIPValidationError(`Invalid router for ${input.displayName}`) + } + } + + try { + const sdkInfo = networkInfo(input.chainSelector) + return { + chainSelector: input.chainSelector, + name: sdkInfo.name, // Auto-populated from SDK + displayName: input.displayName, + router: input.router, + } + } catch { + throw new CCIPValidationError( + `Chain selector ${input.chainSelector} not found in SDK. ` + + `Ensure the chain is registered in ccip-sdk first.`, + ) + } +} + +/** + * Options for creating a registry instance. + */ +export interface RegistryOptions { + /** + * Custom logger for this registry instance. + * If not provided, uses the global logger set via setLogger(). + */ + logger?: Logger + /** + * Skip SDK validation (for testing only). + * @internal + */ + skipValidation?: boolean +} + +/** + * Create an isolated registry instance. + * + * Use for testing or when multiple independent registries are needed. + * Each registry maintains its own state and doesn't affect others. + * + * The `name` field is automatically populated from the SDK's `networkInfo()`. + * + * @param options - Optional configuration including per-registry logger + * + * @example + * ```typescript + * const registry = createRegistry() + * registry.register({ + * chainSelector: 5009297550715157269n, + * displayName: 'Ethereum', + * router: '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D', + * }) + * // Name is auto-populated from SDK: 'ethereum-mainnet' + * const deployment = registry.getByName('ethereum-mainnet') + * ``` + */ +export function createRegistry(options?: RegistryOptions): Registry { + const getLogger = (): Logger => options?.logger ?? logger + const deployments = new Map() + const ccipEnabledSelectors = new Set() + const nameIndex = new Map() // SDK canonical name -> chainSelector + let cache: readonly ChainDeployment[] | null = null + let ccipEnabledCache: readonly CCIPEnabledDeployment[] | null = null + + return { + register(input: ChainDeploymentInput): void { + let deployment: ChainDeployment + if (options?.skipValidation) { + deployment = { + chainSelector: input.chainSelector, + name: `test-chain-${input.chainSelector}`, + displayName: input.displayName, + router: input.router, + } + } else { + deployment = validateAndResolve(input) + } + + cache = null + ccipEnabledCache = null + + if (deployments.has(deployment.chainSelector)) { + const existing = deployments.get(deployment.chainSelector)! + getLogger().warn( + `[ccip-config] Duplicate registration for chain selector ${deployment.chainSelector}. ` + + `Existing: "${existing.name}", New: "${deployment.name}". Using new value.`, + ) + nameIndex.delete(existing.name) + } + + if (deployment.router) { + ccipEnabledSelectors.add(deployment.chainSelector) + } else { + ccipEnabledSelectors.delete(deployment.chainSelector) + } + + const existingSelector = nameIndex.get(deployment.name) + if (existingSelector !== undefined && existingSelector !== deployment.chainSelector) { + getLogger().warn( + `[ccip-config] Name collision: "${deployment.name}" is already registered ` + + `for chain selector ${existingSelector}. Overwriting with ${deployment.chainSelector}.`, + ) + } + + nameIndex.set(deployment.name, deployment.chainSelector) + deployments.set(deployment.chainSelector, Object.freeze({ ...deployment })) + }, + + get(chainSelector: bigint): ChainDeployment | undefined { + return deployments.get(chainSelector) + }, + + getByName(name: string): ChainDeployment | undefined { + const selector = nameIndex.get(name) + return selector !== undefined ? deployments.get(selector) : undefined + }, + + getRouter(chainSelector: bigint): string | undefined { + return deployments.get(chainSelector)?.router + }, + + getAll(): readonly ChainDeployment[] { + if (cache === null) { + cache = Object.freeze(Array.from(deployments.values())) + } + return cache + }, + + getCCIPEnabled(): readonly CCIPEnabledDeployment[] { + if (ccipEnabledCache === null) { + // Build cache from the ccipEnabledSelectors index for O(n) where n = enabled count + const enabled: CCIPEnabledDeployment[] = [] + for (const selector of ccipEnabledSelectors) { + const deployment = deployments.get(selector) + if (deployment && deployment.router) { + // Type assertion is safe here because the if-guard above ensures router exists, + // and ccipEnabledSelectors only contains selectors with routers (maintained by register()) + enabled.push(deployment as CCIPEnabledDeployment) + } + } + ccipEnabledCache = Object.freeze(enabled) + } + return ccipEnabledCache + }, + + getCCIPEnabledCount(): number { + return ccipEnabledSelectors.size + }, + + clear(): void { + deployments.clear() + ccipEnabledSelectors.clear() + nameIndex.clear() + cache = null + ccipEnabledCache = null + }, + } +} + +// Global registry (used by side-effect chain imports) +const globalRegistry = createRegistry() + +/** + * Register a chain deployment to the global registry. + * Called internally by chain modules on import (side-effect). + * + * @internal + */ +export const registerDeployment = globalRegistry.register.bind(globalRegistry) + +/** + * Get all registered deployments from the global registry. + * Returns a frozen array that cannot be mutated. + * + * @returns Frozen array of all registered chain deployments + */ +export const getAllDeployments = globalRegistry.getAll.bind(globalRegistry) + +/** + * Get deployment by chain selector from the global registry. + * + * @param chainSelector - CCIP chain selector + * @returns ChainDeployment or undefined if not found + */ +export const getDeployment = globalRegistry.get.bind(globalRegistry) + +/** + * Get deployment by SDK canonical name from the global registry (O(1) lookup). + * + * @param name - SDK canonical name (e.g., "ethereum-mainnet") + * @returns ChainDeployment or undefined if not found + */ +export const getDeploymentByNameFromRegistry = globalRegistry.getByName.bind(globalRegistry) + +/** + * Get CCIP-enabled deployments from the global registry. + * + * @returns Array of deployments with router addresses + */ +export const getCCIPEnabledDeployments = globalRegistry.getCCIPEnabled.bind(globalRegistry) + +/** + * Get count of CCIP-enabled chains in the global registry. + * + * @returns Number of chains with router addresses + */ +export const getCCIPEnabledCount = globalRegistry.getCCIPEnabledCount.bind(globalRegistry) + +/** + * Clear the global registry. For testing purposes only. + * + * @internal + */ +export const clearRegistry = globalRegistry.clear.bind(globalRegistry) diff --git a/ccip-config/src/types.ts b/ccip-config/src/types.ts new file mode 100644 index 00000000..56831e2a --- /dev/null +++ b/ccip-config/src/types.ts @@ -0,0 +1,36 @@ +/** + * Input for registering a chain deployment. + * The `name` field is auto-populated from SDK if not provided. + */ +export type ChainDeploymentInput = { + /** CCIP chain selector (primary lookup key) */ + readonly chainSelector: bigint + /** Human-readable display name for UI (e.g., "Ethereum", "Arbitrum One") */ + readonly displayName: string + /** CCIP Router contract address (undefined if CCIP not deployed) */ + readonly router?: string +} + +/** + * Deployment configuration for a chain. + * Contains deployment artifacts, NOT protocol constants. + * Protocol data (chainId, family, isTestnet) lives in the SDK. + */ +export type ChainDeployment = { + /** CCIP chain selector (primary lookup key) */ + readonly chainSelector: bigint + /** SDK canonical name (e.g., "ethereum-mainnet") - used for name-based lookups */ + readonly name: string + /** Human-readable display name for UI (e.g., "Ethereum", "Arbitrum One") */ + readonly displayName: string + /** CCIP Router contract address (undefined if CCIP not deployed) */ + readonly router?: string +} + +/** + * ChainDeployment with guaranteed router address. + * Use this type when you need to ensure the chain has CCIP support. + */ +export type CCIPEnabledDeployment = ChainDeployment & { + readonly router: string +} diff --git a/ccip-config/tsconfig.build.json b/ccip-config/tsconfig.build.json new file mode 100644 index 00000000..df31bd1d --- /dev/null +++ b/ccip-config/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": [ + "./src" + ], + "exclude": [ + "node_modules", + "**/*.test.*", + "**/__tests__", + "**/__mocks__" + ] +} diff --git a/ccip-config/tsconfig.json b/ccip-config/tsconfig.json new file mode 100644 index 00000000..d5fa9e75 --- /dev/null +++ b/ccip-config/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "lib": ["ES2023"], + "strict": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "forceConsistentCasingInFileNames": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "verbatimModuleSyntax": true, + "erasableSyntaxOnly": true, + "resolveJsonModule": true, + "noImplicitOverride": true, + "useUnknownInCatchVariables": true, + "noUncheckedIndexedAccess": true + } +} diff --git a/docs/cli/index.md b/docs/cli/index.md index b1b71a5b..56111651 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -369,6 +369,62 @@ ccip-cli lane-latency ethereum-mainnet polygon-mainnet --api-url https://custom- --- +### chains + +List and lookup CCIP chain configuration including router addresses. + +```bash +ccip-cli chains [identifier] [options] +``` + +**Arguments:** +- `identifier` - (Optional) Chain name, chainId, or selector to lookup + +**Options:** +| Option | Alias | Description | +|--------|-------|-------------| +| `--family ` | | Filter by chain family: `evm`, `solana`, `aptos`, `sui`, `ton` | +| `--mainnet` | | Show only mainnets | +| `--testnet` | | Show only testnets | +| `--ccip-only` | | Show only CCIP-enabled chains (with router addresses) | +| `--search ` | `-s` | Fuzzy search chains by name | +| `--interactive` | `-i` | Interactive mode with type-ahead filtering | +| `--json` | | Output as JSON for scripting | +| `--field ` | | Output only a specific field value | +| `--count` | | Show count summary only | + +**Examples:** + +```bash +# List all chains +ccip-cli chains + +# List EVM mainnets with CCIP routers +ccip-cli chains --family evm --mainnet --ccip-only + +# Lookup a specific chain +ccip-cli chains ethereum-mainnet +ccip-cli chains 1 # by EVM chainId +ccip-cli chains 5009297550715157269 # by selector + +# Fuzzy search (typo-tolerant) +ccip-cli chains --search "arbtrum" # finds "arbitrum" + +# Interactive mode - browse and select +ccip-cli chains -i + +# Get just the router address for scripting +ROUTER=$(ccip-cli chains ethereum-mainnet --field router) + +# JSON output for processing +ccip-cli chains --json --family evm --mainnet | jq '.[].router' + +# Count CCIP-enabled chains +ccip-cli chains --count --ccip-only +``` + +--- + ## Output Formats | Format | Use Case | diff --git a/docs/config/index.md b/docs/config/index.md new file mode 100644 index 00000000..8751c7b4 --- /dev/null +++ b/docs/config/index.md @@ -0,0 +1,407 @@ +--- +id: ccip-tools-config +title: CCIP Config +sidebar_label: CCIP Config Overview +sidebar_position: 0 +edit_url: https://github.com/smartcontractkit/ccip-tools-ts/edit/main/docs/config/index.md +--- + +# CCIP Config + +Chain deployment configuration registry for CCIP-enabled chains. + +:::important +This package is provided under an MIT license and is for convenience and illustration purposes only. +::: + +## Overview + +`@chainlink/ccip-config` provides deployment data (router addresses, display names) for CCIP-enabled chains. It is designed to work alongside `@chainlink/ccip-sdk`. + +**Separation of Concerns:** + +| Package | Data Type | Examples | +|---------|-----------|----------| +| `@chainlink/ccip-sdk` | Protocol data | Chain selectors, families, network info | +| `@chainlink/ccip-config` | Deployment data | Router addresses, display names | + +## Installation + +```bash +npm install @chainlink/ccip-config +``` + +## Quick Start + +```typescript +import { getRouter, requireRouter, getDisplayName } from '@chainlink/ccip-config' +import '@chainlink/ccip-config/chains/evm/mainnet' + +// Get Ethereum mainnet router +const router = getRouter(5009297550715157269n) +// => '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D' +``` + +--- + +## Importing Chain Deployments + +Chain deployments are registered via **side-effect imports**. Import only what you need for optimal bundle size: + +### Specific Environments + +```typescript +// EVM chains only +import '@chainlink/ccip-config/chains/evm/mainnet' // EVM mainnets +import '@chainlink/ccip-config/chains/evm/testnet' // EVM testnets +import '@chainlink/ccip-config/chains/evm' // All EVM + +// Non-EVM chains +import '@chainlink/ccip-config/chains/solana/mainnet' +import '@chainlink/ccip-config/chains/solana/testnet' +import '@chainlink/ccip-config/chains/solana' + +import '@chainlink/ccip-config/chains/aptos' +import '@chainlink/ccip-config/chains/sui' +import '@chainlink/ccip-config/chains/ton' +``` + +### All Chains + +```typescript +// Import everything (larger bundle) +import '@chainlink/ccip-config/chains' +``` + +### Available Import Paths + +| Import Path | Description | +|-------------|-------------| +| `@chainlink/ccip-config/chains` | All chains | +| `@chainlink/ccip-config/chains/evm` | All EVM chains | +| `@chainlink/ccip-config/chains/evm/mainnet` | EVM mainnets only | +| `@chainlink/ccip-config/chains/evm/testnet` | EVM testnets only | +| `@chainlink/ccip-config/chains/solana` | All Solana chains | +| `@chainlink/ccip-config/chains/solana/mainnet` | Solana mainnet | +| `@chainlink/ccip-config/chains/solana/testnet` | Solana testnets | +| `@chainlink/ccip-config/chains/aptos` | All Aptos chains | +| `@chainlink/ccip-config/chains/aptos/mainnet` | Aptos mainnet | +| `@chainlink/ccip-config/chains/aptos/testnet` | Aptos testnets | +| `@chainlink/ccip-config/chains/sui` | All Sui chains | +| `@chainlink/ccip-config/chains/sui/mainnet` | Sui mainnet | +| `@chainlink/ccip-config/chains/sui/testnet` | Sui testnets | +| `@chainlink/ccip-config/chains/ton` | All TON chains | +| `@chainlink/ccip-config/chains/ton/mainnet` | TON mainnet | +| `@chainlink/ccip-config/chains/ton/testnet` | TON testnets | + +--- + +## API Reference + +### Lookup Functions + +#### getRouter + +Get router address for a chain. Returns `undefined` if not found. + +```typescript +import { getRouter } from '@chainlink/ccip-config' + +const router = getRouter(5009297550715157269n) +// => '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D' or undefined +``` + +#### requireRouter + +Get router address, throws if not found or not configured. + +```typescript +import { requireRouter } from '@chainlink/ccip-config' + +try { + const router = requireRouter(5009297550715157269n) +} catch (e) { + // CCIPDeploymentNotFoundError or CCIPRouterNotFoundError +} +``` + +#### getDisplayName + +Get human-readable display name for a chain. + +```typescript +import { getDisplayName } from '@chainlink/ccip-config' + +const name = getDisplayName(5009297550715157269n) +// => 'Ethereum' +``` + +#### isCCIPEnabled (Type Guard) + +Type guard to check if a deployment has CCIP router configured. Narrows `ChainDeployment` to `CCIPEnabledDeployment`. + +```typescript +import { getDeployment, isCCIPEnabled } from '@chainlink/ccip-config' + +const deployment = getDeployment(5009297550715157269n) +if (deployment && isCCIPEnabled(deployment)) { + // deployment.router is now string (not string | undefined) + console.log(deployment.router) +} +``` + +#### isCCIPEnabledBySelector + +Check if a chain has CCIP router configured by selector. + +```typescript +import { isCCIPEnabledBySelector } from '@chainlink/ccip-config' + +if (isCCIPEnabledBySelector(5009297550715157269n)) { + // Chain has CCIP router +} +``` + +#### getDeployment / requireDeployment + +Get full deployment object. + +```typescript +import { getDeployment, requireDeployment } from '@chainlink/ccip-config' + +const deployment = getDeployment(5009297550715157269n) +// => { chainSelector: 5009297550715157269n, displayName: 'Ethereum', router: '0x...' } + +const deployment = requireDeployment(5009297550715157269n) +// Throws if not found +``` + +#### getDeploymentByName + +Find deployment by SDK canonical name (case-sensitive, O(1) lookup). + +```typescript +import { getDeploymentByName } from '@chainlink/ccip-config' + +const deployment = getDeploymentByName('ethereum-mainnet') +// => { chainSelector: 5009297550715157269n, name: 'ethereum-mainnet', displayName: 'Ethereum', router: '0x...' } + +// Case-sensitive: 'Ethereum' won't match (that's displayName, not SDK name) +``` + +### List Functions + +#### getAllDeployments + +Get all registered deployments. + +```typescript +import { getAllDeployments } from '@chainlink/ccip-config' +import '@chainlink/ccip-config/chains' + +const deployments = getAllDeployments() +console.log(`Total: ${deployments.length} chains`) +``` + +#### getCCIPEnabledDeployments + +Get only deployments with router addresses configured. + +```typescript +import { getCCIPEnabledDeployments } from '@chainlink/ccip-config' +import '@chainlink/ccip-config/chains' + +const enabled = getCCIPEnabledDeployments() +// Only chains with router addresses +``` + +#### getCCIPEnabledCount + +Get count of CCIP-enabled chains (O(1) operation). + +```typescript +import { getCCIPEnabledCount } from '@chainlink/ccip-config' +import '@chainlink/ccip-config/chains' + +const count = getCCIPEnabledCount() +// => 100+ (number of chains with routers) +``` + +### Registry Functions + +#### createRegistry + +Create an isolated registry instance for testing or advanced use cases. + +```typescript +import { createRegistry } from '@chainlink/ccip-config' + +const registry = createRegistry() + +// Register using real chain selector (name auto-populated from SDK) +registry.register({ + chainSelector: 5009297550715157269n, // Ethereum mainnet + displayName: 'Ethereum', + router: '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D', +}) + +// Use the isolated registry +const deployment = registry.get(5009297550715157269n) +const byName = registry.getByName('ethereum-mainnet') // SDK canonical name +const enabled = registry.getCCIPEnabled() +const count = registry.getCCIPEnabledCount() +registry.clear() + +// For testing with fake chain selectors, use skipValidation +const testRegistry = createRegistry({ skipValidation: true }) +testRegistry.register({ chainSelector: 123n, displayName: 'Test' }) +``` + +--- + +## Types + +### ChainDeploymentInput + +Input type for registration (name is auto-populated from SDK). + +```typescript +type ChainDeploymentInput = { + readonly chainSelector: bigint + readonly displayName: string + readonly router?: string +} +``` + +### ChainDeployment + +Full deployment type with SDK canonical name. + +```typescript +type ChainDeployment = { + readonly chainSelector: bigint + readonly name: string // SDK canonical name (e.g., 'ethereum-mainnet') + readonly displayName: string // Human-readable name for UI + readonly router?: string +} +``` + +### CCIPEnabledDeployment + +A `ChainDeployment` with guaranteed router address. + +```typescript +type CCIPEnabledDeployment = ChainDeployment & { + readonly router: string +} +``` + +### Registry + +Interface for isolated registries. + +```typescript +interface Registry { + register(input: ChainDeploymentInput): void // Name auto-populated from SDK + get(chainSelector: bigint): ChainDeployment | undefined + getByName(name: string): ChainDeployment | undefined // SDK canonical name, O(1) + getRouter(chainSelector: bigint): string | undefined + getAll(): readonly ChainDeployment[] + getCCIPEnabled(): readonly CCIPEnabledDeployment[] + getCCIPEnabledCount(): number + clear(): void +} +``` + +--- + +## Errors + +### CCIPDeploymentNotFoundError + +Thrown when no deployment is found for a chain selector. + +```typescript +import { CCIPDeploymentNotFoundError, ErrorCodes } from '@chainlink/ccip-config' + +try { + requireDeployment(123n) +} catch (e) { + if (e instanceof CCIPDeploymentNotFoundError) { + console.log(e.code) // ErrorCodes.DEPLOYMENT_NOT_FOUND + console.log(e.chainSelector) // 123n + console.log(e.recovery) // Suggestion to import chain data + } +} +``` + +### CCIPRouterNotFoundError + +Thrown when a chain exists but has no router configured. + +```typescript +import { CCIPRouterNotFoundError, ErrorCodes } from '@chainlink/ccip-config' + +try { + requireRouter(someChainWithoutRouter) +} catch (e) { + if (e instanceof CCIPRouterNotFoundError) { + console.log(e.code) // ErrorCodes.ROUTER_NOT_FOUND + console.log(e.chainSelector) + console.log(e.displayName) + } +} +``` + +### Error Codes + +```typescript +import { ErrorCodes } from '@chainlink/ccip-config' + +ErrorCodes.DEPLOYMENT_NOT_FOUND // 'CCIP_DEPLOYMENT_NOT_FOUND' +ErrorCodes.ROUTER_NOT_FOUND // 'CCIP_ROUTER_NOT_FOUND' +``` + +--- + +## Integration with SDK + +Use ccip-config with ccip-sdk for complete chain information: + +```typescript +import { networkInfo } from '@chainlink/ccip-sdk' +import { getRouter } from '@chainlink/ccip-config' +import '@chainlink/ccip-config/chains/evm/mainnet' + +// Get protocol info from SDK +const network = networkInfo('ethereum-mainnet') +console.log(network.chainSelector) // 5009297550715157269n +console.log(network.family) // 'evm' +console.log(network.isTestnet) // false + +// Get deployment info from ccip-config +const router = getRouter(network.chainSelector) +console.log(router) // '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D' +``` + +--- + +## Tree Shaking + +This package is designed for optimal tree-shaking. Only imported chains are included in your bundle: + +```typescript +// Only EVM mainnet chains in bundle (~50 chains) +import '@chainlink/ccip-config/chains/evm/mainnet' + +// vs all chains (~250 chains) +import '@chainlink/ccip-config/chains' +``` + +--- + +## Next Steps + +- [SDK Documentation](../sdk/) - Chain abstraction and message handling +- [CLI Reference](../cli/) - Use `ccip chains` command for chain discovery +- [CCIP Directory](https://docs.chain.link/ccip/directory) - Official router addresses diff --git a/docs/index.md b/docs/index.md index 4c719603..6ec0fc0f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -23,6 +23,7 @@ This tool is provided under an MIT license and is for convenience and illustrati | Manually execute stuck message | CLI | `ccip-cli manualExec 0xTxHash` | | Check supported tokens | CLI | `ccip-cli getSupportedTokens chain router` | | Query lane latency | CLI/SDK | `ccip-cli lane-latency eth-mainnet arb-mainnet` | +| Lookup chain/router info | CLI | `ccip-cli chains ethereum-mainnet` | | Integrate CCIP in your dApp | SDK | Import and use in your code | ## Quick Start @@ -78,26 +79,31 @@ console.log('Fee:', fee.toString()) ## Architecture ``` -┌─────────────────────────────────────────────────────────────────────┐ -│ ccip-tools-ts │ -│ │ -│ ┌──────────────────────────┐ ┌──────────────────────────┐ │ -│ │ │ │ │ │ -│ │ @chainlink/ccip-sdk │◀───────│ @chainlink/ccip-cli │ │ -│ │ │ │ │ │ -│ │ • Chain abstraction │ │ • show, send, manualExec│ │ -│ │ • Message tracking │ │ • parse, getSupportedTokens│ -│ │ • Fee estimation │ │ • lane-latency │ │ -│ │ • Transaction building │ │ • RPC & wallet mgmt │ │ -│ │ • API client │ │ • Output formatting │ │ -│ │ │ │ │ │ -│ └──────────────────────────┘ └──────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ccip-tools-ts │ +│ │ +│ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────────┐ │ +│ │ │ │ │ │ │ │ +│ │ @chainlink/ccip-sdk│ │@chainlink/ccip- │ │ @chainlink/ccip-cli │ │ +│ │ │ │ config │ │ │ │ +│ │ • Chain abstraction│ │ │ │ • show, send, manualExec│ +│ │ • Message tracking │ │ • Router addresses │ │ • chains, parse │ │ +│ │ • Fee estimation │ │ • Display names │ │ • getSupportedTokens │ │ +│ │ • Tx building │ │ • Chain registry │ │ • lane-latency │ │ +│ │ • API client │ │ │ │ • RPC & wallet mgmt │ │ +│ │ │ │ │ │ │ │ +│ └─────────▲──────────┘ └─────────▲──────────┘ └───────────┬────────────┘ │ +│ │ │ │ │ +│ └───────────────────────┴─────────────────────────┘ │ +│ CLI uses both │ +└──────────────────────────────────────────────────────────────────────────────┘ ``` **SDK** - Library for programmatic integration. Supports multiple chain families. -**CLI** - Command-line tool that uses the SDK. Great for debugging, testing, and scripting. +**Config** - Chain deployment registry with router addresses and display names. + +**CLI** - Command-line tool that uses the SDK and Config. Great for debugging, testing, and scripting. ## Supported Chains @@ -121,6 +127,7 @@ console.log('Fee:', fee.toString()) |-------|-------------| | [SDK Guide](./sdk/) | Integrate CCIP in your TypeScript application | | [CLI Reference](./cli/) | Command-line usage and examples | +| [Config Reference](./config/) | Chain deployment registry (router addresses) | | [Contributing](./contributing/) | Development setup and guidelines | | [Adding New Chain](./adding-new-chain) | Implement support for a new blockchain | diff --git a/eslint.config.mjs b/eslint.config.mjs index 6c23517e..3a70a059 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -45,7 +45,7 @@ export default defineConfig( }, { // Ban generic Error constructor - enforce typed error classes - files: ['ccip-sdk/src/**/*.ts', 'ccip-cli/src/**/*.ts'], + files: ['ccip-sdk/src/**/*.ts', 'ccip-cli/src/**/*.ts', 'ccip-config/src/**/*.ts'], ignores: ['**/*.test.ts', '**/__tests__/**', '**/__mocks__/**'], rules: { 'no-restricted-syntax': [ @@ -64,9 +64,9 @@ export default defineConfig( }, }, { - // Cross-platform portability - ban Node.js built-in modules in SDK production code - // SDK must work in both Node.js and browsers. See CONTRIBUTING.md "Cross-Platform Portability" - files: ['ccip-sdk/src/**/*.ts'], + // Cross-platform portability - ban Node.js built-in modules in SDK and ccip-config production code + // SDK and ccip-config must work in both Node.js and browsers. See CONTRIBUTING.md "Cross-Platform Portability" + files: ['ccip-sdk/src/**/*.ts', 'ccip-config/src/**/*.ts'], ignores: ['**/*.test.ts', '**/__tests__/**', '**/__mocks__/**'], rules: { 'import/no-nodejs-modules': [ @@ -146,9 +146,9 @@ export default defineConfig( 'tsdoc/syntax': 'error', // Enforced - all syntax issues have been fixed }, }, - // JSDoc completeness enforcement (both packages, exclude tests) + // JSDoc completeness enforcement (all packages, exclude tests) { - files: ['ccip-sdk/src/**/*.ts', 'ccip-cli/src/**/*.ts'], + files: ['ccip-sdk/src/**/*.ts', 'ccip-cli/src/**/*.ts', 'ccip-config/src/**/*.ts'], ignores: ['**/*.test.ts', '**/__tests__/**', '**/__mocks__/**', '**/idl/**'], plugins: { jsdoc }, rules: { diff --git a/package-lock.json b/package-lock.json index 240e16b1..40fa814f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.93.0", "license": "MIT", "workspaces": [ + "ccip-config", "ccip-sdk", "ccip-cli" ], @@ -35,6 +36,7 @@ "license": "MIT", "dependencies": { "@aptos-labs/ts-sdk": "^5.2.0", + "@chainlink/ccip-config": "^0.93.0", "@chainlink/ccip-sdk": "^0.93.0", "@coral-xyz/anchor": "^0.29.0", "@ethers-ext/signer-ledger": "^6.0.0-beta.1", @@ -46,6 +48,7 @@ "@ton-community/ton-ledger": "^7.3.0", "bs58": "^6.0.0", "ethers": "6.16.0", + "fuse.js": "^7.1.0", "type-fest": "^5.3.1", "yargs": "18.0.0" }, @@ -95,6 +98,34 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, + "ccip-config": { + "name": "@chainlink/ccip-config", + "version": "0.93.0", + "license": "MIT", + "devDependencies": { + "@chainlink/ccip-sdk": "^0.93.0", + "@eslint/js": "^9.39.2", + "@types/node": "25.0.3", + "eslint": "^9.39.2", + "eslint-config-prettier": "10.1.8", + "eslint-import-resolver-typescript": "4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsdoc": "^61.5.0", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-tsdoc": "^0.5.0", + "prettier": "^3.7.4", + "typescript": "5.9.3", + "typescript-eslint": "8.51.0" + }, + "peerDependencies": { + "@chainlink/ccip-sdk": "^0.93.0" + }, + "peerDependenciesMeta": { + "@chainlink/ccip-sdk": { + "optional": true + } + } + }, "ccip-sdk": { "name": "@chainlink/ccip-sdk", "version": "0.93.0", @@ -254,6 +285,10 @@ "resolved": "ccip-cli", "link": true }, + "node_modules/@chainlink/ccip-config": { + "resolved": "ccip-config", + "link": true + }, "node_modules/@chainlink/ccip-sdk": { "resolved": "ccip-sdk", "link": true @@ -6702,6 +6737,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", diff --git a/package.json b/package.json index 0f36e093..1d5073a7 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "repository": "git://github.com/smartcontractkit/ccip-tools-ts.git", "type": "module", "workspaces": [ + "ccip-config", "ccip-sdk", "ccip-cli" ],