From 5f6d971fabeb51bf061991c068edfcfef33282aa Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Tue, 3 Mar 2026 10:29:48 +0100 Subject: [PATCH 01/12] Capabilities scaffolding --- ccip-cli/src/index.ts | 2 +- ccip-sdk/src/api/index.ts | 2 +- ccip-sdk/src/chain.ts | 51 +++++++++++++++++++++++++++++++++++++++ ccip-sdk/src/evm/index.ts | 17 +++++++++++++ ccip-sdk/src/index.ts | 3 ++- 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 74f1d9f3..f063f63b 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 = '1.0.0-0c096d1' +const VERSION = '1.0.0-541b018' // generate:end const globalOpts = { diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index e9c83d1f..902219dd 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -46,7 +46,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.0.0-0c096d1' +export const SDK_VERSION = '1.0.0-541b018' // generate:end /** SDK telemetry header name */ diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index 7e8431f6..278b4d06 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -8,6 +8,7 @@ import { CCIPApiClientNotAvailableError, CCIPChainFamilyMismatchError, CCIPExecTxRevertedError, + CCIPNotImplementedError, CCIPTokenPoolChainConfigNotFoundError, CCIPTransactionNotFinalizedError, } from './errors/index.ts' @@ -173,6 +174,29 @@ export type TokenInfo = { readonly name?: string } +/** + * Available lane capability keys. + * These represent features or thresholds that can be configured per-lane. + */ +export const LaneCapability = { + /** + * Minimum block confirmations required for Faster Time to Finality (FTF). + * When present and non-zero, indicates FTF is enabled on this lane. + */ + MIN_BLOCK_CONFIRMATIONS: 'MIN_BLOCK_CONFIRMATIONS', +} as const +/** Type representing one of the lane capability keys. */ +export type LaneCapability = (typeof LaneCapability)[keyof typeof LaneCapability] + +/** + * Lane capabilities record. + * Maps capability keys to their values. + */ +export type LaneCapabilities = { + /** Minimum block confirmations for FTF. */ + [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: number +} + /** * Options for getBalance query. */ @@ -1050,6 +1074,33 @@ export abstract class Chain { return this.apiClient.getLaneLatency(this.network.chainSelector, destChainSelector) } + /** + * Retrieve capabilities for a lane (onRamp/destChainSelector/token triplet). + * + * @param _opts - Options containing onRamp address, destChainSelector, and optional token + * @returns Promise resolving to partial capabilities record + * + * @throws {@link CCIPNotImplementedError} if not implemented for this chain family + * + * @example Get lane capabilities + * ```typescript + * const caps = await chain.getCapabilities({ + * onRamp: '0x...', + * destChainSelector: 4949039107694359620n, + * }) + * if (caps.MIN_BLOCK_CONFIRMATIONS !== undefined) { + * console.log(`FTF enabled with ${caps.MIN_BLOCK_CONFIRMATIONS} confirmations`) + * } + * ``` + */ + getCapabilities(_opts: { + onRamp: string + destChainSelector: bigint + token?: string + }): Promise> { + return Promise.reject(new CCIPNotImplementedError('getCapabilities')) + } + /** * Default/generic implementation of getExecutionReceipts. * Yields execution receipts for a given offRamp. diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index 0af38625..5ee04f69 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -27,9 +27,11 @@ import type { PickDeep, SetRequired } from 'type-fest' import { type ChainContext, type GetBalanceOpts, + type LaneCapabilities, type LogFilter, type TokenPoolRemote, Chain, + LaneCapability, } from '../chain.ts' import { CCIPAddressInvalidEvmError, @@ -625,6 +627,21 @@ export class EVMChain extends Chain { } } + /** + * {@inheritDoc Chain.getCapabilities} + */ + override getCapabilities(_opts: { + onRamp: string + destChainSelector: bigint + token?: string + }): Promise> { + // TODO: Implement actual capability detection from OnRamp contract + // For now, return stubbed value + return Promise.resolve({ + [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 1, + }) + } + /** * {@inheritDoc Chain.getRouterForOffRamp} * @throws {@link CCIPVersionUnsupportedError} if OffRamp version is not supported diff --git a/ccip-sdk/src/index.ts b/ccip-sdk/src/index.ts index ceeed180..39712f37 100644 --- a/ccip-sdk/src/index.ts +++ b/ccip-sdk/src/index.ts @@ -27,6 +27,7 @@ export type { ChainGetter, ChainStatic, GetBalanceOpts, + LaneCapabilities, LogFilter, RateLimiterState, RegistryTokenConfig, @@ -34,7 +35,7 @@ export type { TokenPoolConfig, TokenPoolRemote, } from './chain.ts' -export { DEFAULT_API_RETRY_CONFIG } from './chain.ts' +export { DEFAULT_API_RETRY_CONFIG, LaneCapability } from './chain.ts' export { calculateManualExecProof, discoverOffRamp } from './execution.ts' export { type EVMExtraArgsV1, From c263f632937ed20e335646381108203481d1e1a8 Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Tue, 3 Mar 2026 11:07:27 +0100 Subject: [PATCH 02/12] Check lane version --- ccip-sdk/src/chain.ts | 15 +++++++++++++-- ccip-sdk/src/evm/index.ts | 21 +++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index 278b4d06..4802c9da 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -192,11 +192,21 @@ export type LaneCapability = (typeof LaneCapability)[keyof typeof LaneCapability * Lane capabilities record. * Maps capability keys to their values. */ -export type LaneCapabilities = { +export interface LaneCapabilities { /** Minimum block confirmations for FTF. */ - [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: number + MIN_BLOCK_CONFIRMATIONS: number } +// Compile-time check: LaneCapability keys and LaneCapabilities keys must match. +// If this errors, the two definitions have diverged. +type _AssertCapabilityKeysMatch = [LaneCapability] extends [keyof LaneCapabilities] + ? [keyof LaneCapabilities] extends [LaneCapability] + ? true + : never + : never +const _capabilityKeysMatch: _AssertCapabilityKeysMatch = true +void _capabilityKeysMatch + /** * Options for getBalance query. */ @@ -1078,6 +1088,7 @@ export abstract class Chain { * Retrieve capabilities for a lane (onRamp/destChainSelector/token triplet). * * @param _opts - Options containing onRamp address, destChainSelector, and optional token + * address (the token to be transferred in a hypothetical message on this lane) * @returns Promise resolving to partial capabilities record * * @throws {@link CCIPNotImplementedError} if not implemented for this chain family diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index 5ee04f69..ce7c1360 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -630,16 +630,25 @@ export class EVMChain extends Chain { /** * {@inheritDoc Chain.getCapabilities} */ - override getCapabilities(_opts: { + override async getCapabilities(opts: { onRamp: string destChainSelector: bigint token?: string }): Promise> { - // TODO: Implement actual capability detection from OnRamp contract - // For now, return stubbed value - return Promise.resolve({ - [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 1, - }) + const [, version] = await this.typeAndVersion(opts.onRamp) + + // FTF only exists on V2_0+ + if (version < CCIPVersion.V2_0) { + return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 0 } + } + + // No token transfer → FTF supported, default 1 confirmation + if (!opts.token) { + return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 1 } + } + + // TODO: Query token pool for token-specific min block confirmations + return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 0 } } /** From 489ecf5f1e0e6722377d15ab8eb7ede382546f32 Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Tue, 3 Mar 2026 11:15:00 +0100 Subject: [PATCH 03/12] Add token pool ABI --- ccip-sdk/src/evm/abi/TokenPool_2_0.ts | 1632 +++++++++++++++++++++++++ 1 file changed, 1632 insertions(+) create mode 100644 ccip-sdk/src/evm/abi/TokenPool_2_0.ts diff --git a/ccip-sdk/src/evm/abi/TokenPool_2_0.ts b/ccip-sdk/src/evm/abi/TokenPool_2_0.ts new file mode 100644 index 00000000..c948e73b --- /dev/null +++ b/ccip-sdk/src/evm/abi/TokenPool_2_0.ts @@ -0,0 +1,1632 @@ +// TODO: track a v2 release tag and the v2.0.0 folder instead of a commit + latest/ folder, once 2.0.0 is released in `chainlink-ccip` +export default [ + // generate: + // fetch('https://github.com/smartcontractkit/chainlink-ccip/raw/refs/heads/develop/ccv/chains/evm/gobindings/generated/latest/token_pool/token_pool.go') + // .then((res) => res.text()) + // .then((body) => body.match(/^\s*ABI: "(.*?)",$/m)?.[1]) + // .then((abi) => JSON.parse(abi.replace(/\\"/g, '"'))) + // .then((obj) => require('util').inspect(obj, {depth:99}).split('\n').slice(1, -1)) + { + type: 'function', + name: 'acceptOwnership', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'addRemotePool', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'remotePoolAddress', + type: 'bytes', + internalType: 'bytes', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'applyChainUpdates', + inputs: [ + { + name: 'remoteChainSelectorsToRemove', + type: 'uint64[]', + internalType: 'uint64[]', + }, + { + name: 'chainsToAdd', + type: 'tuple[]', + internalType: 'struct TokenPool.ChainUpdate[]', + components: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'remotePoolAddresses', + type: 'bytes[]', + internalType: 'bytes[]', + }, + { + name: 'remoteTokenAddress', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'outboundRateLimiterConfig', + type: 'tuple', + internalType: 'struct RateLimiter.Config', + components: [ + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { + name: 'rate', + type: 'uint128', + internalType: 'uint128', + }, + ], + }, + { + name: 'inboundRateLimiterConfig', + type: 'tuple', + internalType: 'struct RateLimiter.Config', + components: [ + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { + name: 'rate', + type: 'uint128', + internalType: 'uint128', + }, + ], + }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'applyTokenTransferFeeConfigUpdates', + inputs: [ + { + name: 'tokenTransferFeeConfigArgs', + type: 'tuple[]', + internalType: 'struct TokenPool.TokenTransferFeeConfigArgs[]', + components: [ + { + name: 'destChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'tokenTransferFeeConfig', + type: 'tuple', + internalType: 'struct IPoolV2.TokenTransferFeeConfig', + components: [ + { + name: 'destGasOverhead', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'destBytesOverhead', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'defaultBlockConfirmationsFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'customBlockConfirmationsFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'defaultBlockConfirmationsTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'customBlockConfirmationsTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + ], + }, + ], + }, + { + name: 'disableTokenTransferFeeConfigs', + type: 'uint64[]', + internalType: 'uint64[]', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'getAdvancedPoolHooks', + inputs: [], + outputs: [ + { + name: 'advancedPoolHook', + type: 'address', + internalType: 'contract IAdvancedPoolHooks', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCurrentRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'customBlockConfirmations', + type: 'bool', + internalType: 'bool', + }, + ], + outputs: [ + { + name: 'outboundRateLimiterState', + type: 'tuple', + internalType: 'struct RateLimiter.TokenBucket', + components: [ + { name: 'tokens', type: 'uint128', internalType: 'uint128' }, + { + name: 'lastUpdated', + type: 'uint32', + internalType: 'uint32', + }, + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { name: 'rate', type: 'uint128', internalType: 'uint128' }, + ], + }, + { + name: 'inboundRateLimiterState', + type: 'tuple', + internalType: 'struct RateLimiter.TokenBucket', + components: [ + { name: 'tokens', type: 'uint128', internalType: 'uint128' }, + { + name: 'lastUpdated', + type: 'uint32', + internalType: 'uint32', + }, + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { name: 'rate', type: 'uint128', internalType: 'uint128' }, + ], + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getDynamicConfig', + inputs: [], + outputs: [ + { name: 'router', type: 'address', internalType: 'address' }, + { + name: 'rateLimitAdmin', + type: 'address', + internalType: 'address', + }, + { name: 'feeAdmin', type: 'address', internalType: 'address' }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getFee', + inputs: [ + { name: '', type: 'address', internalType: 'address' }, + { + name: 'destChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { name: '', type: 'uint256', internalType: 'uint256' }, + { name: '', type: 'address', internalType: 'address' }, + { + name: 'blockConfirmationsRequested', + type: 'uint16', + internalType: 'uint16', + }, + { name: '', type: 'bytes', internalType: 'bytes' }, + ], + outputs: [ + { name: 'feeUSDCents', type: 'uint256', internalType: 'uint256' }, + { + name: 'destGasOverhead', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'destBytesOverhead', + type: 'uint32', + internalType: 'uint32', + }, + { name: 'tokenFeeBps', type: 'uint16', internalType: 'uint16' }, + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getMinBlockConfirmations', + inputs: [], + outputs: [ + { + name: 'minBlockConfirmations', + type: 'uint16', + internalType: 'uint16', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRemotePools', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + outputs: [{ name: '', type: 'bytes[]', internalType: 'bytes[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRemoteToken', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + outputs: [{ name: '', type: 'bytes', internalType: 'bytes' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRequiredCCVs', + inputs: [ + { name: 'localToken', type: 'address', internalType: 'address' }, + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + { + name: 'blockConfirmationsRequested', + type: 'uint16', + internalType: 'uint16', + }, + { name: 'extraData', type: 'bytes', internalType: 'bytes' }, + { + name: 'direction', + type: 'uint8', + internalType: 'enum IPoolV2.MessageDirection', + }, + ], + outputs: [ + { + name: 'requiredCCVs', + type: 'address[]', + internalType: 'address[]', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRmnProxy', + inputs: [], + outputs: [{ name: 'rmnProxy', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getSupportedChains', + inputs: [], + outputs: [{ name: '', type: 'uint64[]', internalType: 'uint64[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getToken', + inputs: [], + outputs: [ + { + name: 'token', + type: 'address', + internalType: 'contract IERC20', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getTokenDecimals', + inputs: [], + outputs: [{ name: 'decimals', type: 'uint8', internalType: 'uint8' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getTokenTransferFeeConfig', + inputs: [ + { name: '', type: 'address', internalType: 'address' }, + { + name: 'destChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { name: '', type: 'uint16', internalType: 'uint16' }, + { name: '', type: 'bytes', internalType: 'bytes' }, + ], + outputs: [ + { + name: 'feeConfig', + type: 'tuple', + internalType: 'struct IPoolV2.TokenTransferFeeConfig', + components: [ + { + name: 'destGasOverhead', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'destBytesOverhead', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'defaultBlockConfirmationsFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'customBlockConfirmationsFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'defaultBlockConfirmationsTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'customBlockConfirmationsTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + ], + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isRemotePool', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'remotePoolAddress', + type: 'bytes', + internalType: 'bytes', + }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isSupportedChain', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isSupportedToken', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'lockOrBurn', + inputs: [ + { + name: 'lockOrBurnIn', + type: 'tuple', + internalType: 'struct Pool.LockOrBurnInV1', + components: [ + { name: 'receiver', type: 'bytes', internalType: 'bytes' }, + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'originalSender', + type: 'address', + internalType: 'address', + }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + { + name: 'localToken', + type: 'address', + internalType: 'address', + }, + ], + }, + ], + outputs: [ + { + name: 'lockOrBurnOutV1', + type: 'tuple', + internalType: 'struct Pool.LockOrBurnOutV1', + components: [ + { + name: 'destTokenAddress', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'destPoolData', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'lockOrBurn', + inputs: [ + { + name: 'lockOrBurnIn', + type: 'tuple', + internalType: 'struct Pool.LockOrBurnInV1', + components: [ + { name: 'receiver', type: 'bytes', internalType: 'bytes' }, + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'originalSender', + type: 'address', + internalType: 'address', + }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + { + name: 'localToken', + type: 'address', + internalType: 'address', + }, + ], + }, + { + name: 'blockConfirmationsRequested', + type: 'uint16', + internalType: 'uint16', + }, + { name: 'tokenArgs', type: 'bytes', internalType: 'bytes' }, + ], + outputs: [ + { + name: '', + type: 'tuple', + internalType: 'struct Pool.LockOrBurnOutV1', + components: [ + { + name: 'destTokenAddress', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'destPoolData', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + { + name: 'destTokenAmount', + type: 'uint256', + internalType: 'uint256', + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'releaseOrMint', + inputs: [ + { + name: 'releaseOrMintIn', + type: 'tuple', + internalType: 'struct Pool.ReleaseOrMintInV1', + components: [ + { + name: 'originalSender', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'receiver', + type: 'address', + internalType: 'address', + }, + { + name: 'sourceDenominatedAmount', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'localToken', + type: 'address', + internalType: 'address', + }, + { + name: 'sourcePoolAddress', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'sourcePoolData', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'offchainTokenData', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + ], + outputs: [ + { + name: '', + type: 'tuple', + internalType: 'struct Pool.ReleaseOrMintOutV1', + components: [ + { + name: 'destinationAmount', + type: 'uint256', + internalType: 'uint256', + }, + ], + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'releaseOrMint', + inputs: [ + { + name: 'releaseOrMintIn', + type: 'tuple', + internalType: 'struct Pool.ReleaseOrMintInV1', + components: [ + { + name: 'originalSender', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'receiver', + type: 'address', + internalType: 'address', + }, + { + name: 'sourceDenominatedAmount', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'localToken', + type: 'address', + internalType: 'address', + }, + { + name: 'sourcePoolAddress', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'sourcePoolData', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'offchainTokenData', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + { + name: 'blockConfirmationsRequested', + type: 'uint16', + internalType: 'uint16', + }, + ], + outputs: [ + { + name: '', + type: 'tuple', + internalType: 'struct Pool.ReleaseOrMintOutV1', + components: [ + { + name: 'destinationAmount', + type: 'uint256', + internalType: 'uint256', + }, + ], + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'removeRemotePool', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'remotePoolAddress', + type: 'bytes', + internalType: 'bytes', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setDynamicConfig', + inputs: [ + { name: 'router', type: 'address', internalType: 'address' }, + { + name: 'rateLimitAdmin', + type: 'address', + internalType: 'address', + }, + { name: 'feeAdmin', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setMinBlockConfirmations', + inputs: [ + { + name: 'minBlockConfirmations', + type: 'uint16', + internalType: 'uint16', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setRateLimitConfig', + inputs: [ + { + name: 'rateLimitConfigArgs', + type: 'tuple[]', + internalType: 'struct TokenPool.RateLimitConfigArgs[]', + components: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'customBlockConfirmations', + type: 'bool', + internalType: 'bool', + }, + { + name: 'outboundRateLimiterConfig', + type: 'tuple', + internalType: 'struct RateLimiter.Config', + components: [ + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { + name: 'rate', + type: 'uint128', + internalType: 'uint128', + }, + ], + }, + { + name: 'inboundRateLimiterConfig', + type: 'tuple', + internalType: 'struct RateLimiter.Config', + components: [ + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { + name: 'rate', + type: 'uint128', + internalType: 'uint128', + }, + ], + }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'supportsInterface', + inputs: [{ name: 'interfaceId', type: 'bytes4', internalType: 'bytes4' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'pure', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'updateAdvancedPoolHooks', + inputs: [ + { + name: 'newHook', + type: 'address', + internalType: 'contract IAdvancedPoolHooks', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'withdrawFeeTokens', + inputs: [ + { + name: 'feeTokens', + type: 'address[]', + internalType: 'address[]', + }, + { name: 'recipient', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'AdvancedPoolHooksUpdated', + inputs: [ + { + name: 'oldHook', + type: 'address', + indexed: false, + internalType: 'contract IAdvancedPoolHooks', + }, + { + name: 'newHook', + type: 'address', + indexed: false, + internalType: 'contract IAdvancedPoolHooks', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'ChainAdded', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: false, + internalType: 'uint64', + }, + { + name: 'remoteToken', + type: 'bytes', + indexed: false, + internalType: 'bytes', + }, + { + name: 'outboundRateLimiterConfig', + type: 'tuple', + indexed: false, + internalType: 'struct RateLimiter.Config', + components: [ + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { name: 'rate', type: 'uint128', internalType: 'uint128' }, + ], + }, + { + name: 'inboundRateLimiterConfig', + type: 'tuple', + indexed: false, + internalType: 'struct RateLimiter.Config', + components: [ + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { name: 'rate', type: 'uint128', internalType: 'uint128' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'ChainRemoved', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: false, + internalType: 'uint64', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'CustomBlockConfirmationsInboundRateLimitConsumed', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: true, + internalType: 'uint64', + }, + { + name: 'token', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'CustomBlockConfirmationsOutboundRateLimitConsumed', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: true, + internalType: 'uint64', + }, + { + name: 'token', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'DynamicConfigSet', + inputs: [ + { + name: 'router', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'rateLimitAdmin', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'feeAdmin', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'FeeTokenWithdrawn', + inputs: [ + { + name: 'receiver', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'feeToken', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'InboundRateLimitConsumed', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: true, + internalType: 'uint64', + }, + { + name: 'token', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'LockedOrBurned', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: true, + internalType: 'uint64', + }, + { + name: 'token', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'sender', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'MinBlockConfirmationsSet', + inputs: [ + { + name: 'minBlockConfirmations', + type: 'uint16', + indexed: false, + internalType: 'uint16', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'OutboundRateLimitConsumed', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: true, + internalType: 'uint64', + }, + { + name: 'token', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'OwnershipTransferRequested', + inputs: [ + { + name: 'from', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'to', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'OwnershipTransferred', + inputs: [ + { + name: 'from', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'to', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RateLimitConfigured', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: true, + internalType: 'uint64', + }, + { + name: 'customBlockConfirmations', + type: 'bool', + indexed: false, + internalType: 'bool', + }, + { + name: 'outboundRateLimiterConfig', + type: 'tuple', + indexed: false, + internalType: 'struct RateLimiter.Config', + components: [ + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { name: 'rate', type: 'uint128', internalType: 'uint128' }, + ], + }, + { + name: 'inboundRateLimiterConfig', + type: 'tuple', + indexed: false, + internalType: 'struct RateLimiter.Config', + components: [ + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { name: 'rate', type: 'uint128', internalType: 'uint128' }, + ], + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'ReleasedOrMinted', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: true, + internalType: 'uint64', + }, + { + name: 'token', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'sender', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'recipient', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RemotePoolAdded', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: true, + internalType: 'uint64', + }, + { + name: 'remotePoolAddress', + type: 'bytes', + indexed: false, + internalType: 'bytes', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RemotePoolRemoved', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: true, + internalType: 'uint64', + }, + { + name: 'remotePoolAddress', + type: 'bytes', + indexed: false, + internalType: 'bytes', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'TokenTransferFeeConfigDeleted', + inputs: [ + { + name: 'destChainSelector', + type: 'uint64', + indexed: true, + internalType: 'uint64', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'TokenTransferFeeConfigUpdated', + inputs: [ + { + name: 'destChainSelector', + type: 'uint64', + indexed: true, + internalType: 'uint64', + }, + { + name: 'tokenTransferFeeConfig', + type: 'tuple', + indexed: false, + internalType: 'struct IPoolV2.TokenTransferFeeConfig', + components: [ + { + name: 'destGasOverhead', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'destBytesOverhead', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'defaultBlockConfirmationsFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'customBlockConfirmationsFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'defaultBlockConfirmationsTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'customBlockConfirmationsTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + ], + }, + ], + anonymous: false, + }, + { type: 'error', name: 'BucketOverfilled', inputs: [] }, + { + type: 'error', + name: 'CallerIsNotARampOnRouter', + inputs: [{ name: 'caller', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'CallerIsNotOwnerOrFeeAdmin', + inputs: [{ name: 'caller', type: 'address', internalType: 'address' }], + }, + { type: 'error', name: 'CannotTransferToSelf', inputs: [] }, + { + type: 'error', + name: 'ChainAlreadyExists', + inputs: [{ name: 'chainSelector', type: 'uint64', internalType: 'uint64' }], + }, + { + type: 'error', + name: 'ChainNotAllowed', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + }, + { type: 'error', name: 'CursedByRMN', inputs: [] }, + { + type: 'error', + name: 'CustomBlockConfirmationsNotEnabled', + inputs: [], + }, + { + type: 'error', + name: 'DisabledNonZeroRateLimit', + inputs: [ + { + name: 'config', + type: 'tuple', + internalType: 'struct RateLimiter.Config', + components: [ + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { name: 'rate', type: 'uint128', internalType: 'uint128' }, + ], + }, + ], + }, + { + type: 'error', + name: 'InvalidDecimalArgs', + inputs: [ + { name: 'expected', type: 'uint8', internalType: 'uint8' }, + { name: 'actual', type: 'uint8', internalType: 'uint8' }, + ], + }, + { + type: 'error', + name: 'InvalidMinBlockConfirmations', + inputs: [ + { name: 'requested', type: 'uint16', internalType: 'uint16' }, + { + name: 'minBlockConfirmations', + type: 'uint16', + internalType: 'uint16', + }, + ], + }, + { + type: 'error', + name: 'InvalidRateLimitRate', + inputs: [ + { + name: 'rateLimiterConfig', + type: 'tuple', + internalType: 'struct RateLimiter.Config', + components: [ + { name: 'isEnabled', type: 'bool', internalType: 'bool' }, + { + name: 'capacity', + type: 'uint128', + internalType: 'uint128', + }, + { name: 'rate', type: 'uint128', internalType: 'uint128' }, + ], + }, + ], + }, + { + type: 'error', + name: 'InvalidRemoteChainDecimals', + inputs: [{ name: 'sourcePoolData', type: 'bytes', internalType: 'bytes' }], + }, + { + type: 'error', + name: 'InvalidRemotePoolForChain', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'remotePoolAddress', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + { + type: 'error', + name: 'InvalidSourcePoolAddress', + inputs: [ + { + name: 'sourcePoolAddress', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + { + type: 'error', + name: 'InvalidToken', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'InvalidTokenTransferFeeConfig', + inputs: [ + { + name: 'destChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + }, + { + type: 'error', + name: 'InvalidTransferFeeBps', + inputs: [{ name: 'bps', type: 'uint256', internalType: 'uint256' }], + }, + { type: 'error', name: 'MismatchedArrayLengths', inputs: [] }, + { type: 'error', name: 'MustBeProposedOwner', inputs: [] }, + { + type: 'error', + name: 'NonExistentChain', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + }, + { type: 'error', name: 'OnlyCallableByOwner', inputs: [] }, + { + type: 'error', + name: 'OverflowDetected', + inputs: [ + { name: 'remoteDecimals', type: 'uint8', internalType: 'uint8' }, + { name: 'localDecimals', type: 'uint8', internalType: 'uint8' }, + { + name: 'remoteAmount', + type: 'uint256', + internalType: 'uint256', + }, + ], + }, + { type: 'error', name: 'OwnerCannotBeZero', inputs: [] }, + { + type: 'error', + name: 'PoolAlreadyAdded', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'remotePoolAddress', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + { + type: 'error', + name: 'SafeERC20FailedOperation', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'TokenMaxCapacityExceeded', + inputs: [ + { name: 'capacity', type: 'uint256', internalType: 'uint256' }, + { name: 'requested', type: 'uint256', internalType: 'uint256' }, + { + name: 'tokenAddress', + type: 'address', + internalType: 'address', + }, + ], + }, + { + type: 'error', + name: 'TokenRateLimitReached', + inputs: [ + { + name: 'minWaitInSeconds', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'available', type: 'uint256', internalType: 'uint256' }, + { + name: 'tokenAddress', + type: 'address', + internalType: 'address', + }, + ], + }, + { + type: 'error', + name: 'Unauthorized', + inputs: [{ name: 'caller', type: 'address', internalType: 'address' }], + }, + { type: 'error', name: 'ZeroAddressInvalid', inputs: [] }, + { type: 'error', name: 'ZeroAddressNotAllowed', inputs: [] }, + // generate:end +] as const From ab9dc5da0ab4e260eac12d9c3dd7633110d2ddf7 Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Tue, 3 Mar 2026 11:30:49 +0100 Subject: [PATCH 04/12] Query token pool for FTF capability --- ccip-cli/src/index.ts | 2 +- ccip-sdk/src/api/index.ts | 2 +- ccip-sdk/src/evm/const.ts | 2 ++ ccip-sdk/src/evm/index.ts | 23 +++++++++++++++++++++-- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index f063f63b..dcff1a05 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 = '1.0.0-541b018' +const VERSION = '1.0.0-c263f63' // generate:end const globalOpts = { diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index 902219dd..043443f3 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -46,7 +46,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.0.0-541b018' +export const SDK_VERSION = '1.0.0-c263f63' // generate:end /** SDK telemetry header name */ diff --git a/ccip-sdk/src/evm/const.ts b/ccip-sdk/src/evm/const.ts index 86815537..f01c755e 100644 --- a/ccip-sdk/src/evm/const.ts +++ b/ccip-sdk/src/evm/const.ts @@ -19,6 +19,7 @@ import OnRamp_2_0_ABI from './abi/OnRamp_2_0.ts' import PriceRegistry_1_2_ABI from './abi/PriceRegistry_1_2.ts' import Router_ABI from './abi/Router.ts' import TokenAdminRegistry_ABI from './abi/TokenAdminRegistry_1_5.ts' +import TokenPool_2_0_ABI from './abi/TokenPool_2_0.ts' export const defaultAbiCoder = AbiCoder.defaultAbiCoder() @@ -38,6 +39,7 @@ export const interfaces = { FeeQuoter: new Interface(FeeQuoter_ABI), TokenPool_v1_5_1: new Interface(TokenPool_1_5_1_ABI), TokenPool_v1_5: new Interface(TokenPool_1_5_ABI), + TokenPool_v2_0: new Interface(TokenPool_2_0_ABI), TokenPool_v1_6: new Interface(TokenPool_1_6_ABI), CommitStore_v1_5: new Interface(CommitStore_1_5_ABI), CommitStore_v1_2: new Interface(CommitStore_1_2_ABI), diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index ce7c1360..7505764b 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -97,6 +97,7 @@ import type OnRamp_1_6_ABI from './abi/OnRamp_1_6.ts' import type OnRamp_2_0_ABI from './abi/OnRamp_2_0.ts' import type Router_ABI from './abi/Router.ts' import type TokenAdminRegistry_1_5_ABI from './abi/TokenAdminRegistry_1_5.ts' +import type TokenPool_2_0_ABI from './abi/TokenPool_2_0.ts' import { CCV_INDEXER_URL, VersionedContractABI, @@ -647,8 +648,26 @@ export class EVMChain extends Chain { return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 1 } } - // TODO: Query token pool for token-specific min block confirmations - return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 0 } + // Resolve token → token pool via OnRamp.getPoolBySourceToken + const onRampContract = new Contract( + opts.onRamp, + interfaces.OnRamp_v2_0, + this.provider, + ) as unknown as TypedContract + const tokenPool = (await onRampContract.getPoolBySourceToken( + opts.destChainSelector, + opts.token, + )) as string + + // Query token pool for min block confirmations + const poolContract = new Contract( + tokenPool, + interfaces.TokenPool_v2_0, + this.provider, + ) as unknown as TypedContract + const minBlockConfirmations = Number(await poolContract.getMinBlockConfirmations()) + + return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: minBlockConfirmations } } /** From 290278109f927fc5109abb4ec1dac097263b929f Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Tue, 3 Mar 2026 12:52:56 +0100 Subject: [PATCH 05/12] Update fork tests --- ccip-sdk/src/evm/fork.test.ts | 82 +++++++++++++++++++++++++++++++++++ ccip-sdk/src/evm/index.ts | 21 +++++---- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/ccip-sdk/src/evm/fork.test.ts b/ccip-sdk/src/evm/fork.test.ts index a919b8b7..c699013a 100644 --- a/ccip-sdk/src/evm/fork.test.ts +++ b/ccip-sdk/src/evm/fork.test.ts @@ -7,6 +7,7 @@ import { anvil } from 'prool/instances' import '../aptos/index.ts' // register Aptos chain family for cross-family message decoding import { CCIPAPIClient } from '../api/index.ts' +import { LaneCapability } from '../chain.ts' import { calculateManualExecProof, discoverOffRamp } from '../execution.ts' import { type ExecutionInput, ExecutionState, MessageStatus } from '../types.ts' import { interfaces } from './const.ts' @@ -47,6 +48,21 @@ const CCIP_MESSAGE_SENT_TOPIC = interfaces.OnRamp_v1_6.getEvent('CCIPMessageSent // Token with pool support on the Sepolia -> Aptos lane const APTOS_SUPPORTED_TOKEN = '0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05' +// ── getCapabilities constants ── + +// v1.6 onRamp: Sepolia -> Fuji +const SEPOLIA_V1_6_ONRAMP = '0x37ea845b0F019eAb760caA59a982B9AcF3A71CAB' +// v2.0 onRamp: Sepolia -> Fuji +const SEPOLIA_V2_0_ONRAMP = '0x0F887309075403d02563CBCbB3D98Fb2ef2D2946' +// v2.0 onRamp: Fuji -> Sepolia +const FUJI_V2_0_ONRAMP = '0x2162318D639BBbC2bc8D1562a7baFA459b9F29BF' +// Token on Sepolia whose pool (BurnMintTokenPool 1.7.0-dev) supports the older +// singular getMinBlockConfirmation(), not the plural getMinBlockConfirmations() +// in our current ABI. This exercises the try-catch fallback path. +const OLD_POOL_TOKEN_SEPOLIA = '0x67f000ca40cb1c6ee3bd2c7fda2fd22ddf56faab' +// Token on Fuji whose pool (LombardTokenPool 2.0.0-dev) DOES support getMinBlockConfirmations +const FTF_TOKEN_FUJI = '0x7FbdC44BfEBDe80C970ba622B678daB36cee31f6' + // ── execute constants ── // Known message stuck in FAILED state on sepolia, sent from fuji (v1.6) @@ -506,6 +522,72 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { ) }) + describe('getCapabilities', () => { + it('should return MIN_BLOCK_CONFIRMATIONS=0 for v1.6 onRamp', async () => { + assert.ok(sepoliaChain, 'sepolia chain should be initialized') + + const caps = await sepoliaChain.getCapabilities({ + onRamp: SEPOLIA_V1_6_ONRAMP, + destChainSelector: FUJI_SELECTOR, + }) + + assert.equal( + caps[LaneCapability.MIN_BLOCK_CONFIRMATIONS], + 0, + 'v1.6 lane should have FTF disabled (MIN_BLOCK_CONFIRMATIONS=0)', + ) + }) + + it('should return MIN_BLOCK_CONFIRMATIONS=1 for v2.0 onRamp without token', async () => { + assert.ok(sepoliaChain, 'sepolia chain should be initialized') + + const caps = await sepoliaChain.getCapabilities({ + onRamp: SEPOLIA_V2_0_ONRAMP, + destChainSelector: FUJI_SELECTOR, + }) + + assert.equal( + caps[LaneCapability.MIN_BLOCK_CONFIRMATIONS], + 1, + 'v2.0 lane without token should default to 1 block confirmation', + ) + }) + + it('should return MIN_BLOCK_CONFIRMATIONS=0 for token with old pool (fallback)', async () => { + assert.ok(sepoliaChain, 'sepolia chain should be initialized') + + const caps = await sepoliaChain.getCapabilities({ + onRamp: SEPOLIA_V2_0_ONRAMP, + destChainSelector: FUJI_SELECTOR, + token: OLD_POOL_TOKEN_SEPOLIA, + }) + + assert.equal( + caps[LaneCapability.MIN_BLOCK_CONFIRMATIONS], + 0, + 'token with old pool should fall back to FTF disabled (MIN_BLOCK_CONFIRMATIONS=0)', + ) + }) + + it('should query token pool for MIN_BLOCK_CONFIRMATIONS on v2.0 pool', async () => { + assert.ok(fujiChain, 'fuji chain should be initialized') + + const caps = await fujiChain.getCapabilities({ + onRamp: FUJI_V2_0_ONRAMP, + destChainSelector: SEPOLIA_SELECTOR, + token: FTF_TOKEN_FUJI, + }) + + const minBlocks = caps[LaneCapability.MIN_BLOCK_CONFIRMATIONS] + console.log(` Lombard pool MIN_BLOCK_CONFIRMATIONS = ${minBlocks}`) + assert.equal( + minBlocks, + 0, + 'Lombard pool should return MIN_BLOCK_CONFIRMATIONS=0 (FTF not enabled)', + ) + }) + }) + // ── State-mutating tests below: keep these last so read-only tests see clean fork state ── describe('sendMessage', () => { diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index 7505764b..689c391b 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -659,15 +659,20 @@ export class EVMChain extends Chain { opts.token, )) as string - // Query token pool for min block confirmations - const poolContract = new Contract( - tokenPool, - interfaces.TokenPool_v2_0, - this.provider, - ) as unknown as TypedContract - const minBlockConfirmations = Number(await poolContract.getMinBlockConfirmations()) + // Query token pool for min block confirmations; older pools may not + // support this function, in which case FTF is not available for this token + try { + const poolContract = new Contract( + tokenPool, + interfaces.TokenPool_v2_0, + this.provider, + ) as unknown as TypedContract + const minBlockConfirmations = Number(await poolContract.getMinBlockConfirmations()) - return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: minBlockConfirmations } + return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: minBlockConfirmations } + } catch { + return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 0 } + } } /** From 3215096545e72f53952b2c7a5bbee3968df2c33e Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Tue, 3 Mar 2026 12:57:52 +0100 Subject: [PATCH 06/12] Fix comment --- ccip-sdk/src/chain.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index 4802c9da..d3ff2bad 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -1099,7 +1099,9 @@ export abstract class Chain { * onRamp: '0x...', * destChainSelector: 4949039107694359620n, * }) - * if (caps.MIN_BLOCK_CONFIRMATIONS !== undefined) { + * // FTF is enabled when MIN_BLOCK_CONFIRMATIONS is defined and > 0. + * // A value of 0 means FTF is disabled for this lane. + * if (caps.MIN_BLOCK_CONFIRMATIONS != null && caps.MIN_BLOCK_CONFIRMATIONS > 0) { * console.log(`FTF enabled with ${caps.MIN_BLOCK_CONFIRMATIONS} confirmations`) * } * ``` From b55a162668b3c79a9b9024a443964b75a082d117 Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Tue, 3 Mar 2026 16:54:04 +0100 Subject: [PATCH 07/12] Address review comments --- ccip-sdk/src/evm/index.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index 689c391b..ef95a797 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -15,6 +15,7 @@ import { getAddress, hexlify, isBytesLike, + isError, isHexString, keccak256, toBeHex, @@ -47,6 +48,7 @@ import { CCIPNotImplementedError, CCIPSourceChainUnsupportedError, CCIPTokenNotConfiguredError, + CCIPTokenNotFoundError, CCIPTokenPoolChainConfigNotFoundError, CCIPTransactionNotFoundError, CCIPVersionFeatureUnavailableError, @@ -659,6 +661,12 @@ export class EVMChain extends Chain { opts.token, )) as string + if (!tokenPool || tokenPool === ZeroAddress) + throw new CCIPTokenNotFoundError(opts.token, { + context: { onRamp: opts.onRamp, destChainSelector: String(opts.destChainSelector) }, + recovery: 'Verify the token is supported on this lane', + }) + // Query token pool for min block confirmations; older pools may not // support this function, in which case FTF is not available for this token try { @@ -670,8 +678,11 @@ export class EVMChain extends Chain { const minBlockConfirmations = Number(await poolContract.getMinBlockConfirmations()) return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: minBlockConfirmations } - } catch { - return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 0 } + } catch (err) { + if (isError(err, 'CALL_EXCEPTION')) { + return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 0 } + } + throw err } } From de8f90fb6d4c238fe21ff32a7955f790a5bed71f Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Wed, 4 Mar 2026 10:30:04 +0100 Subject: [PATCH 08/12] Rename to lane features --- ccip-sdk/src/chain.ts | 42 +++++++++++++++++------------------ ccip-sdk/src/evm/fork.test.ts | 22 +++++++++--------- ccip-sdk/src/evm/index.ts | 18 +++++++-------- ccip-sdk/src/index.ts | 4 ++-- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index d3ff2bad..aed086c0 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -175,37 +175,37 @@ export type TokenInfo = { } /** - * Available lane capability keys. + * Available lane feature keys. * These represent features or thresholds that can be configured per-lane. */ -export const LaneCapability = { +export const LaneFeature = { /** * Minimum block confirmations required for Faster Time to Finality (FTF). * When present and non-zero, indicates FTF is enabled on this lane. */ MIN_BLOCK_CONFIRMATIONS: 'MIN_BLOCK_CONFIRMATIONS', } as const -/** Type representing one of the lane capability keys. */ -export type LaneCapability = (typeof LaneCapability)[keyof typeof LaneCapability] +/** Type representing one of the lane feature keys. */ +export type LaneFeature = (typeof LaneFeature)[keyof typeof LaneFeature] /** - * Lane capabilities record. - * Maps capability keys to their values. + * Lane features record. + * Maps feature keys to their values. */ -export interface LaneCapabilities { +export interface LaneFeatures { /** Minimum block confirmations for FTF. */ MIN_BLOCK_CONFIRMATIONS: number } -// Compile-time check: LaneCapability keys and LaneCapabilities keys must match. +// Compile-time check: LaneFeature keys and LaneFeatures keys must match. // If this errors, the two definitions have diverged. -type _AssertCapabilityKeysMatch = [LaneCapability] extends [keyof LaneCapabilities] - ? [keyof LaneCapabilities] extends [LaneCapability] +type _AssertFeatureKeysMatch = [LaneFeature] extends [keyof LaneFeatures] + ? [keyof LaneFeatures] extends [LaneFeature] ? true : never : never -const _capabilityKeysMatch: _AssertCapabilityKeysMatch = true -void _capabilityKeysMatch +const _featureKeysMatch: _AssertFeatureKeysMatch = true +void _featureKeysMatch /** * Options for getBalance query. @@ -1085,33 +1085,33 @@ export abstract class Chain { } /** - * Retrieve capabilities for a lane (onRamp/destChainSelector/token triplet). + * Retrieve features for a lane (onRamp/destChainSelector/token triplet). * * @param _opts - Options containing onRamp address, destChainSelector, and optional token * address (the token to be transferred in a hypothetical message on this lane) - * @returns Promise resolving to partial capabilities record + * @returns Promise resolving to partial lane features record * * @throws {@link CCIPNotImplementedError} if not implemented for this chain family * - * @example Get lane capabilities + * @example Get lane features * ```typescript - * const caps = await chain.getCapabilities({ + * const features = await chain.getLaneFeatures({ * onRamp: '0x...', * destChainSelector: 4949039107694359620n, * }) * // FTF is enabled when MIN_BLOCK_CONFIRMATIONS is defined and > 0. * // A value of 0 means FTF is disabled for this lane. - * if (caps.MIN_BLOCK_CONFIRMATIONS != null && caps.MIN_BLOCK_CONFIRMATIONS > 0) { - * console.log(`FTF enabled with ${caps.MIN_BLOCK_CONFIRMATIONS} confirmations`) + * if (features.MIN_BLOCK_CONFIRMATIONS != null && features.MIN_BLOCK_CONFIRMATIONS > 0) { + * console.log(`FTF enabled with ${features.MIN_BLOCK_CONFIRMATIONS} confirmations`) * } * ``` */ - getCapabilities(_opts: { + getLaneFeatures(_opts: { onRamp: string destChainSelector: bigint token?: string - }): Promise> { - return Promise.reject(new CCIPNotImplementedError('getCapabilities')) + }): Promise> { + return Promise.reject(new CCIPNotImplementedError('getLaneFeatures')) } /** diff --git a/ccip-sdk/src/evm/fork.test.ts b/ccip-sdk/src/evm/fork.test.ts index c699013a..c85d2d03 100644 --- a/ccip-sdk/src/evm/fork.test.ts +++ b/ccip-sdk/src/evm/fork.test.ts @@ -7,7 +7,7 @@ import { anvil } from 'prool/instances' import '../aptos/index.ts' // register Aptos chain family for cross-family message decoding import { CCIPAPIClient } from '../api/index.ts' -import { LaneCapability } from '../chain.ts' +import { LaneFeature } from '../chain.ts' import { calculateManualExecProof, discoverOffRamp } from '../execution.ts' import { type ExecutionInput, ExecutionState, MessageStatus } from '../types.ts' import { interfaces } from './const.ts' @@ -48,7 +48,7 @@ const CCIP_MESSAGE_SENT_TOPIC = interfaces.OnRamp_v1_6.getEvent('CCIPMessageSent // Token with pool support on the Sepolia -> Aptos lane const APTOS_SUPPORTED_TOKEN = '0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05' -// ── getCapabilities constants ── +// ── getLaneFeatures constants ── // v1.6 onRamp: Sepolia -> Fuji const SEPOLIA_V1_6_ONRAMP = '0x37ea845b0F019eAb760caA59a982B9AcF3A71CAB' @@ -522,17 +522,17 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { ) }) - describe('getCapabilities', () => { + describe('getLaneFeatures', () => { it('should return MIN_BLOCK_CONFIRMATIONS=0 for v1.6 onRamp', async () => { assert.ok(sepoliaChain, 'sepolia chain should be initialized') - const caps = await sepoliaChain.getCapabilities({ + const features = await sepoliaChain.getLaneFeatures({ onRamp: SEPOLIA_V1_6_ONRAMP, destChainSelector: FUJI_SELECTOR, }) assert.equal( - caps[LaneCapability.MIN_BLOCK_CONFIRMATIONS], + features[LaneFeature.MIN_BLOCK_CONFIRMATIONS], 0, 'v1.6 lane should have FTF disabled (MIN_BLOCK_CONFIRMATIONS=0)', ) @@ -541,13 +541,13 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { it('should return MIN_BLOCK_CONFIRMATIONS=1 for v2.0 onRamp without token', async () => { assert.ok(sepoliaChain, 'sepolia chain should be initialized') - const caps = await sepoliaChain.getCapabilities({ + const features = await sepoliaChain.getLaneFeatures({ onRamp: SEPOLIA_V2_0_ONRAMP, destChainSelector: FUJI_SELECTOR, }) assert.equal( - caps[LaneCapability.MIN_BLOCK_CONFIRMATIONS], + features[LaneFeature.MIN_BLOCK_CONFIRMATIONS], 1, 'v2.0 lane without token should default to 1 block confirmation', ) @@ -556,14 +556,14 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { it('should return MIN_BLOCK_CONFIRMATIONS=0 for token with old pool (fallback)', async () => { assert.ok(sepoliaChain, 'sepolia chain should be initialized') - const caps = await sepoliaChain.getCapabilities({ + const features = await sepoliaChain.getLaneFeatures({ onRamp: SEPOLIA_V2_0_ONRAMP, destChainSelector: FUJI_SELECTOR, token: OLD_POOL_TOKEN_SEPOLIA, }) assert.equal( - caps[LaneCapability.MIN_BLOCK_CONFIRMATIONS], + features[LaneFeature.MIN_BLOCK_CONFIRMATIONS], 0, 'token with old pool should fall back to FTF disabled (MIN_BLOCK_CONFIRMATIONS=0)', ) @@ -572,13 +572,13 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { it('should query token pool for MIN_BLOCK_CONFIRMATIONS on v2.0 pool', async () => { assert.ok(fujiChain, 'fuji chain should be initialized') - const caps = await fujiChain.getCapabilities({ + const features = await fujiChain.getLaneFeatures({ onRamp: FUJI_V2_0_ONRAMP, destChainSelector: SEPOLIA_SELECTOR, token: FTF_TOKEN_FUJI, }) - const minBlocks = caps[LaneCapability.MIN_BLOCK_CONFIRMATIONS] + const minBlocks = features[LaneFeature.MIN_BLOCK_CONFIRMATIONS] console.log(` Lombard pool MIN_BLOCK_CONFIRMATIONS = ${minBlocks}`) assert.equal( minBlocks, diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index ef95a797..87de8649 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -28,11 +28,11 @@ import type { PickDeep, SetRequired } from 'type-fest' import { type ChainContext, type GetBalanceOpts, - type LaneCapabilities, + type LaneFeatures, type LogFilter, type TokenPoolRemote, Chain, - LaneCapability, + LaneFeature, } from '../chain.ts' import { CCIPAddressInvalidEvmError, @@ -631,23 +631,23 @@ export class EVMChain extends Chain { } /** - * {@inheritDoc Chain.getCapabilities} + * {@inheritDoc Chain.getLaneFeatures} */ - override async getCapabilities(opts: { + override async getLaneFeatures(opts: { onRamp: string destChainSelector: bigint token?: string - }): Promise> { + }): Promise> { const [, version] = await this.typeAndVersion(opts.onRamp) // FTF only exists on V2_0+ if (version < CCIPVersion.V2_0) { - return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 0 } + return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: 0 } } // No token transfer → FTF supported, default 1 confirmation if (!opts.token) { - return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 1 } + return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: 1 } } // Resolve token → token pool via OnRamp.getPoolBySourceToken @@ -677,10 +677,10 @@ export class EVMChain extends Chain { ) as unknown as TypedContract const minBlockConfirmations = Number(await poolContract.getMinBlockConfirmations()) - return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: minBlockConfirmations } + return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: minBlockConfirmations } } catch (err) { if (isError(err, 'CALL_EXCEPTION')) { - return { [LaneCapability.MIN_BLOCK_CONFIRMATIONS]: 0 } + return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: 0 } } throw err } diff --git a/ccip-sdk/src/index.ts b/ccip-sdk/src/index.ts index 39712f37..700ef70d 100644 --- a/ccip-sdk/src/index.ts +++ b/ccip-sdk/src/index.ts @@ -27,7 +27,7 @@ export type { ChainGetter, ChainStatic, GetBalanceOpts, - LaneCapabilities, + LaneFeatures, LogFilter, RateLimiterState, RegistryTokenConfig, @@ -35,7 +35,7 @@ export type { TokenPoolConfig, TokenPoolRemote, } from './chain.ts' -export { DEFAULT_API_RETRY_CONFIG, LaneCapability } from './chain.ts' +export { DEFAULT_API_RETRY_CONFIG, LaneFeature } from './chain.ts' export { calculateManualExecProof, discoverOffRamp } from './execution.ts' export { type EVMExtraArgsV1, From fcf417fbabc10d302a1f8263fc77f24756df057e Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Wed, 4 Mar 2026 10:55:08 +0100 Subject: [PATCH 09/12] Pass router instead of onRamp to getLaneFeatures --- ccip-cli/src/index.ts | 2 +- ccip-sdk/src/api/index.ts | 2 +- ccip-sdk/src/chain.ts | 8 ++++---- ccip-sdk/src/evm/abi/TokenPool_2_0.ts | 6 +++++- ccip-sdk/src/evm/fork.test.ts | 22 ++++++++++------------ ccip-sdk/src/evm/index.ts | 9 +++++---- 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index dcff1a05..4837a19f 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 = '1.0.0-c263f63' +const VERSION = '1.0.0-de8f90f' // generate:end const globalOpts = { diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index 043443f3..760dd381 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -46,7 +46,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.0.0-c263f63' +export const SDK_VERSION = '1.0.0-de8f90f' // generate:end /** SDK telemetry header name */ diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index aed086c0..9c3e30e5 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -1085,9 +1085,9 @@ export abstract class Chain { } /** - * Retrieve features for a lane (onRamp/destChainSelector/token triplet). + * Retrieve features for a lane (router/destChainSelector/token triplet). * - * @param _opts - Options containing onRamp address, destChainSelector, and optional token + * @param _opts - Options containing router address, destChainSelector, and optional token * address (the token to be transferred in a hypothetical message on this lane) * @returns Promise resolving to partial lane features record * @@ -1096,7 +1096,7 @@ export abstract class Chain { * @example Get lane features * ```typescript * const features = await chain.getLaneFeatures({ - * onRamp: '0x...', + * router: '0x...', * destChainSelector: 4949039107694359620n, * }) * // FTF is enabled when MIN_BLOCK_CONFIRMATIONS is defined and > 0. @@ -1107,7 +1107,7 @@ export abstract class Chain { * ``` */ getLaneFeatures(_opts: { - onRamp: string + router: string destChainSelector: bigint token?: string }): Promise> { diff --git a/ccip-sdk/src/evm/abi/TokenPool_2_0.ts b/ccip-sdk/src/evm/abi/TokenPool_2_0.ts index c948e73b..a5ea7a15 100644 --- a/ccip-sdk/src/evm/abi/TokenPool_2_0.ts +++ b/ccip-sdk/src/evm/abi/TokenPool_2_0.ts @@ -337,7 +337,11 @@ export default [ type: 'uint64', internalType: 'uint64', }, - { name: 'amount', type: 'uint256', internalType: 'uint256' }, + { + name: 'sourceDenominatedAmount', + type: 'uint256', + internalType: 'uint256', + }, { name: 'blockConfirmationsRequested', type: 'uint16', diff --git a/ccip-sdk/src/evm/fork.test.ts b/ccip-sdk/src/evm/fork.test.ts index c85d2d03..28e070dc 100644 --- a/ccip-sdk/src/evm/fork.test.ts +++ b/ccip-sdk/src/evm/fork.test.ts @@ -50,12 +50,10 @@ const APTOS_SUPPORTED_TOKEN = '0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05' // ── getLaneFeatures constants ── -// v1.6 onRamp: Sepolia -> Fuji -const SEPOLIA_V1_6_ONRAMP = '0x37ea845b0F019eAb760caA59a982B9AcF3A71CAB' -// v2.0 onRamp: Sepolia -> Fuji -const SEPOLIA_V2_0_ONRAMP = '0x0F887309075403d02563CBCbB3D98Fb2ef2D2946' -// v2.0 onRamp: Fuji -> Sepolia -const FUJI_V2_0_ONRAMP = '0x2162318D639BBbC2bc8D1562a7baFA459b9F29BF' +// v2.0 router for Sepolia -> Fuji lane +const SEPOLIA_V2_0_ROUTER = '0xc0f457e615348708FaAB3B40ECC26Badb32B7b30' +// v2.0 router for Fuji -> Sepolia lane +const FUJI_V2_0_ROUTER = '0xE7b62d27D6DDca525FE2e1ea526905EbfB36a1e1' // Token on Sepolia whose pool (BurnMintTokenPool 1.7.0-dev) supports the older // singular getMinBlockConfirmation(), not the plural getMinBlockConfirmations() // in our current ABI. This exercises the try-catch fallback path. @@ -523,11 +521,11 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { }) describe('getLaneFeatures', () => { - it('should return MIN_BLOCK_CONFIRMATIONS=0 for v1.6 onRamp', async () => { + it('should return MIN_BLOCK_CONFIRMATIONS=0 for v1.6 router', async () => { assert.ok(sepoliaChain, 'sepolia chain should be initialized') const features = await sepoliaChain.getLaneFeatures({ - onRamp: SEPOLIA_V1_6_ONRAMP, + router: SEPOLIA_V1_6_ROUTER, destChainSelector: FUJI_SELECTOR, }) @@ -538,11 +536,11 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { ) }) - it('should return MIN_BLOCK_CONFIRMATIONS=1 for v2.0 onRamp without token', async () => { + it('should return MIN_BLOCK_CONFIRMATIONS=1 for v2.0 router without token', async () => { assert.ok(sepoliaChain, 'sepolia chain should be initialized') const features = await sepoliaChain.getLaneFeatures({ - onRamp: SEPOLIA_V2_0_ONRAMP, + router: SEPOLIA_V2_0_ROUTER, destChainSelector: FUJI_SELECTOR, }) @@ -557,7 +555,7 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { assert.ok(sepoliaChain, 'sepolia chain should be initialized') const features = await sepoliaChain.getLaneFeatures({ - onRamp: SEPOLIA_V2_0_ONRAMP, + router: SEPOLIA_V2_0_ROUTER, destChainSelector: FUJI_SELECTOR, token: OLD_POOL_TOKEN_SEPOLIA, }) @@ -573,7 +571,7 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { assert.ok(fujiChain, 'fuji chain should be initialized') const features = await fujiChain.getLaneFeatures({ - onRamp: FUJI_V2_0_ONRAMP, + router: FUJI_V2_0_ROUTER, destChainSelector: SEPOLIA_SELECTOR, token: FTF_TOKEN_FUJI, }) diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index 87de8649..4b7e6c2d 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -634,11 +634,12 @@ export class EVMChain extends Chain { * {@inheritDoc Chain.getLaneFeatures} */ override async getLaneFeatures(opts: { - onRamp: string + router: string destChainSelector: bigint token?: string }): Promise> { - const [, version] = await this.typeAndVersion(opts.onRamp) + const onRamp = await this.getOnRampForRouter(opts.router, opts.destChainSelector) + const [, version] = await this.typeAndVersion(onRamp) // FTF only exists on V2_0+ if (version < CCIPVersion.V2_0) { @@ -652,7 +653,7 @@ export class EVMChain extends Chain { // Resolve token → token pool via OnRamp.getPoolBySourceToken const onRampContract = new Contract( - opts.onRamp, + onRamp, interfaces.OnRamp_v2_0, this.provider, ) as unknown as TypedContract @@ -663,7 +664,7 @@ export class EVMChain extends Chain { if (!tokenPool || tokenPool === ZeroAddress) throw new CCIPTokenNotFoundError(opts.token, { - context: { onRamp: opts.onRamp, destChainSelector: String(opts.destChainSelector) }, + context: { router: opts.router, destChainSelector: String(opts.destChainSelector) }, recovery: 'Verify the token is supported on this lane', }) From a8b89591614080fb693c2ba8f83ece30fa4578ab Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Wed, 4 Mar 2026 15:14:52 +0100 Subject: [PATCH 10/12] Address review comments --- ccip-sdk/src/evm/fork.test.ts | 8 ++++---- ccip-sdk/src/evm/index.ts | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ccip-sdk/src/evm/fork.test.ts b/ccip-sdk/src/evm/fork.test.ts index 28e070dc..60488d36 100644 --- a/ccip-sdk/src/evm/fork.test.ts +++ b/ccip-sdk/src/evm/fork.test.ts @@ -531,8 +531,8 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { assert.equal( features[LaneFeature.MIN_BLOCK_CONFIRMATIONS], - 0, - 'v1.6 lane should have FTF disabled (MIN_BLOCK_CONFIRMATIONS=0)', + undefined, + 'v1.6 lane should not include MIN_BLOCK_CONFIRMATIONS (FTF does not exist pre-v2.0)', ) }) @@ -562,8 +562,8 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { assert.equal( features[LaneFeature.MIN_BLOCK_CONFIRMATIONS], - 0, - 'token with old pool should fall back to FTF disabled (MIN_BLOCK_CONFIRMATIONS=0)', + undefined, + 'token with old pool should not include MIN_BLOCK_CONFIRMATIONS (pool does not support FTF)', ) }) diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index 4b7e6c2d..1c55f027 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -41,6 +41,7 @@ import { CCIPContractNotRouterError, CCIPContractTypeInvalidError, CCIPDataFormatUnsupportedError, + CCIPError, CCIPExecTxNotConfirmedError, CCIPExecTxRevertedError, CCIPHasherVersionUnsupportedError, @@ -643,7 +644,7 @@ export class EVMChain extends Chain { // FTF only exists on V2_0+ if (version < CCIPVersion.V2_0) { - return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: 0 } + return {} } // No token transfer → FTF supported, default 1 confirmation @@ -681,9 +682,9 @@ export class EVMChain extends Chain { return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: minBlockConfirmations } } catch (err) { if (isError(err, 'CALL_EXCEPTION')) { - return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: 0 } + return {} } - throw err + throw CCIPError.from(err, 'UNKNOWN') } } From 0040264f1db4898bc8109e08b64e872c67ac123b Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Wed, 4 Mar 2026 15:26:07 +0100 Subject: [PATCH 11/12] restore MIN_BLOCK_CONFIRMATIONS of 0 for old pools --- ccip-sdk/src/evm/fork.test.ts | 4 ++-- ccip-sdk/src/evm/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ccip-sdk/src/evm/fork.test.ts b/ccip-sdk/src/evm/fork.test.ts index 60488d36..f9d42452 100644 --- a/ccip-sdk/src/evm/fork.test.ts +++ b/ccip-sdk/src/evm/fork.test.ts @@ -562,8 +562,8 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { assert.equal( features[LaneFeature.MIN_BLOCK_CONFIRMATIONS], - undefined, - 'token with old pool should not include MIN_BLOCK_CONFIRMATIONS (pool does not support FTF)', + 0, + 'token with old pool should have FTF disabled (MIN_BLOCK_CONFIRMATIONS=0)', ) }) diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index 1c55f027..5067f060 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -682,7 +682,7 @@ export class EVMChain extends Chain { return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: minBlockConfirmations } } catch (err) { if (isError(err, 'CALL_EXCEPTION')) { - return {} + return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: 0 } } throw CCIPError.from(err, 'UNKNOWN') } From eef1e3f7467c5dff0c8374faa4589c6e030aa2c7 Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Wed, 4 Mar 2026 16:16:22 +0100 Subject: [PATCH 12/12] Clean up features type --- ccip-sdk/src/chain.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index 9c3e30e5..25eba802 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -192,21 +192,11 @@ export type LaneFeature = (typeof LaneFeature)[keyof typeof LaneFeature] * Lane features record. * Maps feature keys to their values. */ -export interface LaneFeatures { +export interface LaneFeatures extends Record { /** Minimum block confirmations for FTF. */ MIN_BLOCK_CONFIRMATIONS: number } -// Compile-time check: LaneFeature keys and LaneFeatures keys must match. -// If this errors, the two definitions have diverged. -type _AssertFeatureKeysMatch = [LaneFeature] extends [keyof LaneFeatures] - ? [keyof LaneFeatures] extends [LaneFeature] - ? true - : never - : never -const _featureKeysMatch: _AssertFeatureKeysMatch = true -void _featureKeysMatch - /** * Options for getBalance query. */