diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 74f1d9f3..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-0c096d1' +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 e9c83d1f..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-0c096d1' +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 7e8431f6..25eba802 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 feature keys. + * These represent features or thresholds that can be configured per-lane. + */ +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 feature keys. */ +export type LaneFeature = (typeof LaneFeature)[keyof typeof LaneFeature] + +/** + * Lane features record. + * Maps feature keys to their values. + */ +export interface LaneFeatures extends Record { + /** Minimum block confirmations for FTF. */ + MIN_BLOCK_CONFIRMATIONS: number +} + /** * Options for getBalance query. */ @@ -1050,6 +1074,36 @@ export abstract class Chain { return this.apiClient.getLaneLatency(this.network.chainSelector, destChainSelector) } + /** + * Retrieve features for a lane (router/destChainSelector/token triplet). + * + * @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 + * + * @throws {@link CCIPNotImplementedError} if not implemented for this chain family + * + * @example Get lane features + * ```typescript + * const features = await chain.getLaneFeatures({ + * router: '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 (features.MIN_BLOCK_CONFIRMATIONS != null && features.MIN_BLOCK_CONFIRMATIONS > 0) { + * console.log(`FTF enabled with ${features.MIN_BLOCK_CONFIRMATIONS} confirmations`) + * } + * ``` + */ + getLaneFeatures(_opts: { + router: string + destChainSelector: bigint + token?: string + }): Promise> { + return Promise.reject(new CCIPNotImplementedError('getLaneFeatures')) + } + /** * Default/generic implementation of getExecutionReceipts. * Yields execution receipts for a given offRamp. 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..a5ea7a15 --- /dev/null +++ b/ccip-sdk/src/evm/abi/TokenPool_2_0.ts @@ -0,0 +1,1636 @@ +// 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: 'sourceDenominatedAmount', + 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 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/fork.test.ts b/ccip-sdk/src/evm/fork.test.ts index a919b8b7..f9d42452 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 { LaneFeature } 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,19 @@ 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' +// ── getLaneFeatures constants ── + +// 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. +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 +520,72 @@ describe('EVM Fork Tests', { skip, timeout: 180_000 }, () => { ) }) + describe('getLaneFeatures', () => { + 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({ + router: SEPOLIA_V1_6_ROUTER, + destChainSelector: FUJI_SELECTOR, + }) + + assert.equal( + features[LaneFeature.MIN_BLOCK_CONFIRMATIONS], + undefined, + 'v1.6 lane should not include MIN_BLOCK_CONFIRMATIONS (FTF does not exist pre-v2.0)', + ) + }) + + 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({ + router: SEPOLIA_V2_0_ROUTER, + destChainSelector: FUJI_SELECTOR, + }) + + assert.equal( + features[LaneFeature.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 features = await sepoliaChain.getLaneFeatures({ + router: SEPOLIA_V2_0_ROUTER, + destChainSelector: FUJI_SELECTOR, + token: OLD_POOL_TOKEN_SEPOLIA, + }) + + assert.equal( + features[LaneFeature.MIN_BLOCK_CONFIRMATIONS], + 0, + 'token with old pool should have 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 features = await fujiChain.getLaneFeatures({ + router: FUJI_V2_0_ROUTER, + destChainSelector: SEPOLIA_SELECTOR, + token: FTF_TOKEN_FUJI, + }) + + const minBlocks = features[LaneFeature.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 0af38625..5067f060 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, @@ -27,9 +28,11 @@ import type { PickDeep, SetRequired } from 'type-fest' import { type ChainContext, type GetBalanceOpts, + type LaneFeatures, type LogFilter, type TokenPoolRemote, Chain, + LaneFeature, } from '../chain.ts' import { CCIPAddressInvalidEvmError, @@ -38,6 +41,7 @@ import { CCIPContractNotRouterError, CCIPContractTypeInvalidError, CCIPDataFormatUnsupportedError, + CCIPError, CCIPExecTxNotConfirmedError, CCIPExecTxRevertedError, CCIPHasherVersionUnsupportedError, @@ -45,6 +49,7 @@ import { CCIPNotImplementedError, CCIPSourceChainUnsupportedError, CCIPTokenNotConfiguredError, + CCIPTokenNotFoundError, CCIPTokenPoolChainConfigNotFoundError, CCIPTransactionNotFoundError, CCIPVersionFeatureUnavailableError, @@ -95,6 +100,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, @@ -625,6 +631,63 @@ export class EVMChain extends Chain { } } + /** + * {@inheritDoc Chain.getLaneFeatures} + */ + override async getLaneFeatures(opts: { + router: string + destChainSelector: bigint + token?: string + }): Promise> { + 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) { + return {} + } + + // No token transfer → FTF supported, default 1 confirmation + if (!opts.token) { + return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: 1 } + } + + // Resolve token → token pool via OnRamp.getPoolBySourceToken + const onRampContract = new Contract( + onRamp, + interfaces.OnRamp_v2_0, + this.provider, + ) as unknown as TypedContract + const tokenPool = (await onRampContract.getPoolBySourceToken( + opts.destChainSelector, + opts.token, + )) as string + + if (!tokenPool || tokenPool === ZeroAddress) + throw new CCIPTokenNotFoundError(opts.token, { + context: { router: opts.router, 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 { + const poolContract = new Contract( + tokenPool, + interfaces.TokenPool_v2_0, + this.provider, + ) as unknown as TypedContract + const minBlockConfirmations = Number(await poolContract.getMinBlockConfirmations()) + + return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: minBlockConfirmations } + } catch (err) { + if (isError(err, 'CALL_EXCEPTION')) { + return { [LaneFeature.MIN_BLOCK_CONFIRMATIONS]: 0 } + } + throw CCIPError.from(err, 'UNKNOWN') + } + } + /** * {@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..700ef70d 100644 --- a/ccip-sdk/src/index.ts +++ b/ccip-sdk/src/index.ts @@ -27,6 +27,7 @@ export type { ChainGetter, ChainStatic, GetBalanceOpts, + LaneFeatures, 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, LaneFeature } from './chain.ts' export { calculateManualExecProof, discoverOffRamp } from './execution.ts' export { type EVMExtraArgsV1,