diff --git a/.gitignore b/.gitignore index 7e32b04e..2963edda 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,6 @@ ccip-api-ref/docs-api/v1/* !ccip-api-ref/docs-api/v1/sidebar.d.ts # Canton CLI config -canton-config.json \ No newline at end of file +canton-config.json + +pnpm-lock.yaml \ No newline at end of file diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index 8d443952..2dfff934 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -27,6 +27,14 @@ "types": "./dist/all-chains.d.ts", "default": "./dist/all-chains.js" }, + "./cct/evm": { + "types": "./dist/cct/evm/index.d.ts", + "default": "./dist/cct/evm/index.js" + }, + "./cct/solana": { + "types": "./dist/cct/solana/index.d.ts", + "default": "./dist/cct/solana/index.js" + }, "./dist/*": "./dist/*", "./src/*": "./src/*" }, @@ -67,6 +75,10 @@ "dependencies": { "@aptos-labs/ts-sdk": "^6.3.1", "@coral-xyz/anchor": "^0.29.0", + "@metaplex-foundation/mpl-token-metadata": "3.4.0", + "@metaplex-foundation/umi": "1.5.1", + "@metaplex-foundation/umi-bundle-defaults": "1.5.1", + "@metaplex-foundation/umi-web3js-adapters": "1.5.1", "@mysten/bcs": "^2.1.0", "@mysten/sui": "^2.23.1", "@noble/hashes": "^2.2.0", diff --git a/ccip-sdk/src/cct/errors.ts b/ccip-sdk/src/cct/errors.ts new file mode 100644 index 00000000..3b9f25fe --- /dev/null +++ b/ccip-sdk/src/cct/errors.ts @@ -0,0 +1,233 @@ +/** + * CCT-specific error classes for write operations (validate → encode → submit). + * Shared CCIP errors (`CCIPWalletInvalidError`, etc.) live in `../errors/`. + * + * @packageDocumentation + */ + +import { type CCIPErrorOptions, CCIPError, CCIPErrorCode } from '../errors/index.ts' + +// Parameter validation + +/** + * Thrown before any RPC when operation params fail validation. Permanent. + * + * @example + * ```typescript + * try { + * await cct.setPool({ tokenAddress: 'not-an-address', poolAddress, address, wallet }) + * } catch (error) { + * if (error instanceof CCTParamsInvalidError) { + * console.log(`Invalid ${error.context.operation} param "${error.context.param}"`) + * } + * } + * ``` + */ +export class CCTParamsInvalidError extends CCIPError { + override readonly name = 'CCTParamsInvalidError' + /** Creates a params-invalid error. */ + constructor(operation: string, param: string, reason: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CCT_PARAMS_INVALID, + `Invalid ${operation} parameter "${param}": ${reason}`, + { + ...options, + isTransient: false, + context: { ...options?.context, operation, param, reason }, + }, + ) + } +} + +// Transaction submission + +/** + * Thrown when a CCT write fails before broadcast, the transaction reverts after mining, + * or it mines without the expected effect (e.g. a deployment that produced no contract + * address). Pre-broadcast failures (signing/RPC) may set `isTransient: true` for network + * errors; reverts and post-mining anomalies are permanent and include `context.txHash`. + * + * @example + * ```typescript + * try { + * await cct.setPool({ ...opts, wallet }) + * } catch (error) { + * if (error instanceof CCTTxFailedError) { + * console.log(`${error.context.operation} failed: ${error.context.reason}`) + * } + * } + * ``` + */ +export class CCTTxFailedError extends CCIPError { + override readonly name = 'CCTTxFailedError' + /** Creates a tx-failed error. */ + constructor(operation: string, reason: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.CCT_TX_FAILED, `${operation} failed: ${reason}`, { + ...options, + isTransient: options?.isTransient ?? false, + context: { ...options?.context, operation, reason }, + }) + } +} + +/** + * Thrown when a transaction was broadcast but not confirmed within the timeout. + * Transient — it may still mine; check `context.txHash` before resubmitting. + * + * @example + * ```typescript + * try { + * await cct.setPool({ ...opts, wallet }) + * } catch (error) { + * if (error instanceof CCTTxNotConfirmedError) { + * console.log(`Not confirmed (tx ${error.context.txHash}); retry in ${error.retryAfterMs}ms`) + * } + * } + * ``` + */ +export class CCTTxNotConfirmedError extends CCIPError { + override readonly name = 'CCTTxNotConfirmedError' + /** Creates a tx-not-confirmed error. */ + constructor(operation: string, txHash: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CCT_TX_NOT_CONFIRMED, + `${operation} transaction not confirmed within timeout: ${txHash}`, + { + ...options, + isTransient: true, + retryAfterMs: 5000, + context: { ...options?.context, operation, txHash }, + }, + ) + } +} + +// Contract version dispatch + +/** + * Thrown when the contract at an address is not of the expected type. + * + * @example + * ```typescript + * try { + * await cct.transferOwnership({ poolAddress, newOwner, wallet }) + * } catch (error) { + * if (error instanceof CCTContractTypeInvalidError) { + * console.log(`Expected ${error.context.expected} at ${error.context.address}, got "${error.context.actual}"`) + * } + * } + * ``` + */ +export class CCTContractTypeInvalidError extends CCIPError { + override readonly name = 'CCTContractTypeInvalidError' + /** + * Creates a contract-type-invalid error. `reason` is appended to the message and kept in + * `context`; pass it when `actual` is a recognized type rejected on its own grounds, so the + * message does not read as "wrong address". + */ + constructor( + address: string, + expected: string, + actual: string, + reason?: string, + options?: CCIPErrorOptions, + ) { + super( + CCIPErrorCode.CONTRACT_TYPE_INVALID, + `Expected a ${expected} contract at ${address}, got "${actual}"` + + (reason ? ` — ${reason}` : ''), + { + ...options, + isTransient: false, + context: { ...options?.context, address, expected, actual, ...(reason && { reason }) }, + }, + ) + } +} + +/** + * Thrown when a contract reports a version string the SDK does not recognize. Permanent. + * + * @example + * ```typescript + * try { + * await cct.transferOwnership({ poolAddress, newOwner, wallet }) + * } catch (error) { + * if (error instanceof CCTContractVersionUnsupportedError) { + * console.log(`Unsupported ${error.context.contractType} version: ${error.context.version}`) + * } + * } + * ``` + */ +export class CCTContractVersionUnsupportedError extends CCIPError { + override readonly name = 'CCTContractVersionUnsupportedError' + /** Creates a contract-version-unsupported error. */ + constructor(contractType: string, version: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CCT_CONTRACT_VERSION_UNSUPPORTED, + `Unsupported ${contractType} version: ${version}`, + { + ...options, + isTransient: false, + context: { ...options?.context, contractType, version }, + }, + ) + } +} + +/** + * Thrown when no implementation is registered for an operation at or below the contract's + * version (floor-match miss). Permanent for that contract version. + * + * @example + * ```typescript + * try { + * await cct.transferOwnership({ poolAddress, newOwner, wallet }) + * } catch (error) { + * if (error instanceof CCTOperationUnsupportedError) { + * console.log(`${error.context.operation} unsupported at version ${error.context.version}`) + * } + * } + * ``` + */ +export class CCTOperationUnsupportedError extends CCIPError { + override readonly name = 'CCTOperationUnsupportedError' + /** Creates an operation-unsupported error. */ + constructor(operation: string, version: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CCT_OPERATION_UNSUPPORTED, + `${operation} is not supported at contract version ${version}`, + { + ...options, + isTransient: false, + context: { ...options?.context, operation, version }, + }, + ) + } +} + +/** + * Thrown when CCT account data cannot be decoded. + * + * @example + * ```typescript + * try { + * await cct.getTokenPoolState({ tokenAddress: mint, poolType: 'burn-mint' }) + * } catch (error) { + * if (error instanceof CCTDataDecodeError) { + * console.log(error.message) + * } + * } + * ``` + */ +export class CCTDataDecodeError extends CCIPError { + override readonly name = 'CCTDataDecodeError' + /** Creates a CCT data decode error. */ + constructor(account: string, options?: CCIPErrorOptions) { + super(CCIPErrorCode.CCT_DATA_DECODE_FAILED, `Unable to decode CCT data at ${account}`, { + ...options, + isTransient: false, + context: { ...options?.context, account }, + }) + } +} diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/burn-mint-token-pool-and-proxy.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/burn-mint-token-pool-and-proxy.ts new file mode 100644 index 00000000..e04b953f --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/burn-mint-token-pool-and-proxy.ts @@ -0,0 +1,1055 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v1_5_0/burn_mint_token_pool_and_proxy.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + inputs: [ + { + internalType: 'contract IBurnMintERC20', + name: 'token', + type: 'address', + }, + { + internalType: 'address[]', + name: 'allowlist', + type: 'address[]', + }, + { internalType: 'address', name: 'rmnProxy', type: 'address' }, + { internalType: 'address', name: 'router', type: 'address' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [ + { internalType: 'uint256', name: 'capacity', type: 'uint256' }, + { internalType: 'uint256', name: 'requested', type: 'uint256' }, + ], + name: 'AggregateValueMaxCapacityExceeded', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'minWaitInSeconds', + type: 'uint256', + }, + { internalType: 'uint256', name: 'available', type: 'uint256' }, + ], + name: 'AggregateValueRateLimitReached', + type: 'error', + }, + { inputs: [], name: 'AllowListNotEnabled', type: 'error' }, + { inputs: [], name: 'BucketOverfilled', type: 'error' }, + { + inputs: [{ internalType: 'address', name: 'caller', type: 'address' }], + name: 'CallerIsNotARampOnRouter', + type: 'error', + }, + { + inputs: [{ internalType: 'uint64', name: 'chainSelector', type: 'uint64' }], + name: 'ChainAlreadyExists', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'ChainNotAllowed', + type: 'error', + }, + { inputs: [], name: 'CursedByRMN', type: 'error' }, + { + inputs: [ + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.Config', + name: 'config', + type: 'tuple', + }, + ], + name: 'DisabledNonZeroRateLimit', + type: 'error', + }, + { + inputs: [ + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.Config', + name: 'rateLimiterConfig', + type: 'tuple', + }, + ], + name: 'InvalidRateLimitRate', + type: 'error', + }, + { + inputs: [ + { + internalType: 'bytes', + name: 'sourcePoolAddress', + type: 'bytes', + }, + ], + name: 'InvalidSourcePoolAddress', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'token', type: 'address' }], + name: 'InvalidToken', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'NonExistentChain', + type: 'error', + }, + { inputs: [], name: 'RateLimitMustBeDisabled', type: 'error' }, + { + inputs: [{ internalType: 'address', name: 'sender', type: 'address' }], + name: 'SenderNotAllowed', + type: 'error', + }, + { + inputs: [ + { internalType: 'uint256', name: 'capacity', type: 'uint256' }, + { internalType: 'uint256', name: 'requested', type: 'uint256' }, + { + internalType: 'address', + name: 'tokenAddress', + type: 'address', + }, + ], + name: 'TokenMaxCapacityExceeded', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'minWaitInSeconds', + type: 'uint256', + }, + { internalType: 'uint256', name: 'available', type: 'uint256' }, + { + internalType: 'address', + name: 'tokenAddress', + type: 'address', + }, + ], + name: 'TokenRateLimitReached', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'caller', type: 'address' }], + name: 'Unauthorized', + type: 'error', + }, + { inputs: [], name: 'ZeroAddressNotAllowed', type: 'error' }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'AllowListAdd', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'AllowListRemove', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Burned', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + indexed: false, + internalType: 'bytes', + name: 'remoteToken', + type: 'bytes', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + indexed: false, + internalType: 'struct RateLimiter.Config', + name: 'outboundRateLimiterConfig', + type: 'tuple', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + indexed: false, + internalType: 'struct RateLimiter.Config', + name: 'inboundRateLimiterConfig', + type: 'tuple', + }, + ], + name: 'ChainAdded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + indexed: false, + internalType: 'struct RateLimiter.Config', + name: 'outboundRateLimiterConfig', + type: 'tuple', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + indexed: false, + internalType: 'struct RateLimiter.Config', + name: 'inboundRateLimiterConfig', + type: 'tuple', + }, + ], + name: 'ChainConfigured', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'ChainRemoved', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + indexed: false, + internalType: 'struct RateLimiter.Config', + name: 'config', + type: 'tuple', + }, + ], + name: 'ConfigChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'contract IPoolPriorTo1_5', + name: 'oldPool', + type: 'address', + }, + { + indexed: false, + internalType: 'contract IPoolPriorTo1_5', + name: 'newPool', + type: 'address', + }, + ], + name: 'LegacyPoolChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Locked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Minted', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'from', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'to', + type: 'address', + }, + ], + name: 'OwnershipTransferRequested', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'from', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'to', + type: 'address', + }, + ], + name: 'OwnershipTransferred', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Released', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + indexed: false, + internalType: 'bytes', + name: 'previousPoolAddress', + type: 'bytes', + }, + { + indexed: false, + internalType: 'bytes', + name: 'remotePoolAddress', + type: 'bytes', + }, + ], + name: 'RemotePoolSet', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'oldRouter', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'newRouter', + type: 'address', + }, + ], + name: 'RouterUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'tokens', + type: 'uint256', + }, + ], + name: 'TokensConsumed', + type: 'event', + }, + { + inputs: [], + name: 'acceptOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address[]', name: 'removes', type: 'address[]' }, + { internalType: 'address[]', name: 'adds', type: 'address[]' }, + ], + name: 'applyAllowListUpdates', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { internalType: 'bool', name: 'allowed', type: 'bool' }, + { + internalType: 'bytes', + name: 'remotePoolAddress', + type: 'bytes', + }, + { + internalType: 'bytes', + name: 'remoteTokenAddress', + type: 'bytes', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'rate', + type: 'uint128', + }, + ], + internalType: 'struct RateLimiter.Config', + name: 'outboundRateLimiterConfig', + type: 'tuple', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'rate', + type: 'uint128', + }, + ], + internalType: 'struct RateLimiter.Config', + name: 'inboundRateLimiterConfig', + type: 'tuple', + }, + ], + internalType: 'struct TokenPool.ChainUpdate[]', + name: 'chains', + type: 'tuple[]', + }, + ], + name: 'applyChainUpdates', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'getAllowList', + outputs: [{ internalType: 'address[]', name: '', type: 'address[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAllowListEnabled', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'getCurrentInboundRateLimiterState', + outputs: [ + { + components: [ + { internalType: 'uint128', name: 'tokens', type: 'uint128' }, + { + internalType: 'uint32', + name: 'lastUpdated', + type: 'uint32', + }, + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.TokenBucket', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'getCurrentOutboundRateLimiterState', + outputs: [ + { + components: [ + { internalType: 'uint128', name: 'tokens', type: 'uint128' }, + { + internalType: 'uint32', + name: 'lastUpdated', + type: 'uint32', + }, + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.TokenBucket', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint64', name: '', type: 'uint64' }], + name: 'getOnRamp', + outputs: [ + { + internalType: 'address', + name: 'onRampAddress', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPreviousPool', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRateLimitAdmin', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'getRemotePool', + outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'getRemoteToken', + outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRmnProxy', + outputs: [{ internalType: 'address', name: 'rmnProxy', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRouter', + outputs: [{ internalType: 'address', name: 'router', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getSupportedChains', + outputs: [{ internalType: 'uint64[]', name: '', type: 'uint64[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getToken', + outputs: [ + { + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'sourceChainSelector', + type: 'uint64', + }, + { internalType: 'address', name: 'offRamp', type: 'address' }, + ], + name: 'isOffRamp', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'isSupportedChain', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'token', type: 'address' }], + name: 'isSupportedToken', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'bytes', name: 'receiver', type: 'bytes' }, + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + internalType: 'address', + name: 'originalSender', + type: 'address', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + internalType: 'address', + name: 'localToken', + type: 'address', + }, + ], + internalType: 'struct Pool.LockOrBurnInV1', + name: 'lockOrBurnIn', + type: 'tuple', + }, + ], + name: 'lockOrBurn', + outputs: [ + { + components: [ + { + internalType: 'bytes', + name: 'destTokenAddress', + type: 'bytes', + }, + { + internalType: 'bytes', + name: 'destPoolData', + type: 'bytes', + }, + ], + internalType: 'struct Pool.LockOrBurnOutV1', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'bytes', + name: 'originalSender', + type: 'bytes', + }, + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + internalType: 'address', + name: 'receiver', + type: 'address', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + internalType: 'address', + name: 'localToken', + type: 'address', + }, + { + internalType: 'bytes', + name: 'sourcePoolAddress', + type: 'bytes', + }, + { + internalType: 'bytes', + name: 'sourcePoolData', + type: 'bytes', + }, + { + internalType: 'bytes', + name: 'offchainTokenData', + type: 'bytes', + }, + ], + internalType: 'struct Pool.ReleaseOrMintInV1', + name: 'releaseOrMintIn', + type: 'tuple', + }, + ], + name: 'releaseOrMint', + outputs: [ + { + components: [ + { + internalType: 'uint256', + name: 'destinationAmount', + type: 'uint256', + }, + ], + internalType: 'struct Pool.ReleaseOrMintOutV1', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.Config', + name: 'outboundConfig', + type: 'tuple', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.Config', + name: 'inboundConfig', + type: 'tuple', + }, + ], + name: 'setChainRateLimiterConfig', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'contract IPoolPriorTo1_5', + name: 'prevPool', + type: 'address', + }, + ], + name: 'setPreviousPool', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'rateLimitAdmin', + type: 'address', + }, + ], + name: 'setRateLimitAdmin', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + internalType: 'bytes', + name: 'remotePoolAddress', + type: 'bytes', + }, + ], + name: 'setRemotePool', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'newRouter', type: 'address' }], + name: 'setRouter', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'to', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'typeAndVersion', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/lock-release-token-pool-and-proxy.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/lock-release-token-pool-and-proxy.ts new file mode 100644 index 00000000..4400c01b --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/lock-release-token-pool-and-proxy.ts @@ -0,0 +1,1141 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v1_5_0/lock_release_token_pool_and_proxy.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + inputs: [ + { + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + { + internalType: 'address[]', + name: 'allowlist', + type: 'address[]', + }, + { internalType: 'address', name: 'rmnProxy', type: 'address' }, + { internalType: 'bool', name: 'acceptLiquidity', type: 'bool' }, + { internalType: 'address', name: 'router', type: 'address' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [ + { internalType: 'uint256', name: 'capacity', type: 'uint256' }, + { internalType: 'uint256', name: 'requested', type: 'uint256' }, + ], + name: 'AggregateValueMaxCapacityExceeded', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'minWaitInSeconds', + type: 'uint256', + }, + { internalType: 'uint256', name: 'available', type: 'uint256' }, + ], + name: 'AggregateValueRateLimitReached', + type: 'error', + }, + { inputs: [], name: 'AllowListNotEnabled', type: 'error' }, + { inputs: [], name: 'BucketOverfilled', type: 'error' }, + { + inputs: [{ internalType: 'address', name: 'caller', type: 'address' }], + name: 'CallerIsNotARampOnRouter', + type: 'error', + }, + { + inputs: [{ internalType: 'uint64', name: 'chainSelector', type: 'uint64' }], + name: 'ChainAlreadyExists', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'ChainNotAllowed', + type: 'error', + }, + { inputs: [], name: 'CursedByRMN', type: 'error' }, + { + inputs: [ + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.Config', + name: 'config', + type: 'tuple', + }, + ], + name: 'DisabledNonZeroRateLimit', + type: 'error', + }, + { inputs: [], name: 'InsufficientLiquidity', type: 'error' }, + { + inputs: [ + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.Config', + name: 'rateLimiterConfig', + type: 'tuple', + }, + ], + name: 'InvalidRateLimitRate', + type: 'error', + }, + { + inputs: [ + { + internalType: 'bytes', + name: 'sourcePoolAddress', + type: 'bytes', + }, + ], + name: 'InvalidSourcePoolAddress', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'token', type: 'address' }], + name: 'InvalidToken', + type: 'error', + }, + { inputs: [], name: 'LiquidityNotAccepted', type: 'error' }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'NonExistentChain', + type: 'error', + }, + { inputs: [], name: 'RateLimitMustBeDisabled', type: 'error' }, + { + inputs: [{ internalType: 'address', name: 'sender', type: 'address' }], + name: 'SenderNotAllowed', + type: 'error', + }, + { + inputs: [ + { internalType: 'uint256', name: 'capacity', type: 'uint256' }, + { internalType: 'uint256', name: 'requested', type: 'uint256' }, + { + internalType: 'address', + name: 'tokenAddress', + type: 'address', + }, + ], + name: 'TokenMaxCapacityExceeded', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'minWaitInSeconds', + type: 'uint256', + }, + { internalType: 'uint256', name: 'available', type: 'uint256' }, + { + internalType: 'address', + name: 'tokenAddress', + type: 'address', + }, + ], + name: 'TokenRateLimitReached', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'caller', type: 'address' }], + name: 'Unauthorized', + type: 'error', + }, + { inputs: [], name: 'ZeroAddressNotAllowed', type: 'error' }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'AllowListAdd', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'AllowListRemove', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Burned', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + indexed: false, + internalType: 'bytes', + name: 'remoteToken', + type: 'bytes', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + indexed: false, + internalType: 'struct RateLimiter.Config', + name: 'outboundRateLimiterConfig', + type: 'tuple', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + indexed: false, + internalType: 'struct RateLimiter.Config', + name: 'inboundRateLimiterConfig', + type: 'tuple', + }, + ], + name: 'ChainAdded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + indexed: false, + internalType: 'struct RateLimiter.Config', + name: 'outboundRateLimiterConfig', + type: 'tuple', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + indexed: false, + internalType: 'struct RateLimiter.Config', + name: 'inboundRateLimiterConfig', + type: 'tuple', + }, + ], + name: 'ChainConfigured', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'ChainRemoved', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + indexed: false, + internalType: 'struct RateLimiter.Config', + name: 'config', + type: 'tuple', + }, + ], + name: 'ConfigChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'contract IPoolPriorTo1_5', + name: 'oldPool', + type: 'address', + }, + { + indexed: false, + internalType: 'contract IPoolPriorTo1_5', + name: 'newPool', + type: 'address', + }, + ], + name: 'LegacyPoolChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'provider', + type: 'address', + }, + { + indexed: true, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'LiquidityAdded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'provider', + type: 'address', + }, + { + indexed: true, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'LiquidityRemoved', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Locked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Minted', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'from', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'to', + type: 'address', + }, + ], + name: 'OwnershipTransferRequested', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'from', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'to', + type: 'address', + }, + ], + name: 'OwnershipTransferred', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Released', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + indexed: false, + internalType: 'bytes', + name: 'previousPoolAddress', + type: 'bytes', + }, + { + indexed: false, + internalType: 'bytes', + name: 'remotePoolAddress', + type: 'bytes', + }, + ], + name: 'RemotePoolSet', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'oldRouter', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'newRouter', + type: 'address', + }, + ], + name: 'RouterUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'tokens', + type: 'uint256', + }, + ], + name: 'TokensConsumed', + type: 'event', + }, + { + inputs: [], + name: 'acceptOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address[]', name: 'removes', type: 'address[]' }, + { internalType: 'address[]', name: 'adds', type: 'address[]' }, + ], + name: 'applyAllowListUpdates', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { internalType: 'bool', name: 'allowed', type: 'bool' }, + { + internalType: 'bytes', + name: 'remotePoolAddress', + type: 'bytes', + }, + { + internalType: 'bytes', + name: 'remoteTokenAddress', + type: 'bytes', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'rate', + type: 'uint128', + }, + ], + internalType: 'struct RateLimiter.Config', + name: 'outboundRateLimiterConfig', + type: 'tuple', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'rate', + type: 'uint128', + }, + ], + internalType: 'struct RateLimiter.Config', + name: 'inboundRateLimiterConfig', + type: 'tuple', + }, + ], + internalType: 'struct TokenPool.ChainUpdate[]', + name: 'chains', + type: 'tuple[]', + }, + ], + name: 'applyChainUpdates', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'canAcceptLiquidity', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAllowList', + outputs: [{ internalType: 'address[]', name: '', type: 'address[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getAllowListEnabled', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'getCurrentInboundRateLimiterState', + outputs: [ + { + components: [ + { internalType: 'uint128', name: 'tokens', type: 'uint128' }, + { + internalType: 'uint32', + name: 'lastUpdated', + type: 'uint32', + }, + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.TokenBucket', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'getCurrentOutboundRateLimiterState', + outputs: [ + { + components: [ + { internalType: 'uint128', name: 'tokens', type: 'uint128' }, + { + internalType: 'uint32', + name: 'lastUpdated', + type: 'uint32', + }, + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.TokenBucket', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint64', name: '', type: 'uint64' }], + name: 'getOnRamp', + outputs: [ + { + internalType: 'address', + name: 'onRampAddress', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getPreviousPool', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRateLimitAdmin', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRebalancer', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'getRemotePool', + outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'getRemoteToken', + outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRmnProxy', + outputs: [{ internalType: 'address', name: 'rmnProxy', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getRouter', + outputs: [{ internalType: 'address', name: 'router', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getSupportedChains', + outputs: [{ internalType: 'uint64[]', name: '', type: 'uint64[]' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getToken', + outputs: [ + { + internalType: 'contract IERC20', + name: 'token', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'sourceChainSelector', + type: 'uint64', + }, + { internalType: 'address', name: 'offRamp', type: 'address' }, + ], + name: 'isOffRamp', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + ], + name: 'isSupportedChain', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'token', type: 'address' }], + name: 'isSupportedToken', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'bytes', name: 'receiver', type: 'bytes' }, + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + internalType: 'address', + name: 'originalSender', + type: 'address', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + internalType: 'address', + name: 'localToken', + type: 'address', + }, + ], + internalType: 'struct Pool.LockOrBurnInV1', + name: 'lockOrBurnIn', + type: 'tuple', + }, + ], + name: 'lockOrBurn', + outputs: [ + { + components: [ + { + internalType: 'bytes', + name: 'destTokenAddress', + type: 'bytes', + }, + { + internalType: 'bytes', + name: 'destPoolData', + type: 'bytes', + }, + ], + internalType: 'struct Pool.LockOrBurnOutV1', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], + name: 'provideLiquidity', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'bytes', + name: 'originalSender', + type: 'bytes', + }, + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + internalType: 'address', + name: 'receiver', + type: 'address', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + internalType: 'address', + name: 'localToken', + type: 'address', + }, + { + internalType: 'bytes', + name: 'sourcePoolAddress', + type: 'bytes', + }, + { + internalType: 'bytes', + name: 'sourcePoolData', + type: 'bytes', + }, + { + internalType: 'bytes', + name: 'offchainTokenData', + type: 'bytes', + }, + ], + internalType: 'struct Pool.ReleaseOrMintInV1', + name: 'releaseOrMintIn', + type: 'tuple', + }, + ], + name: 'releaseOrMint', + outputs: [ + { + components: [ + { + internalType: 'uint256', + name: 'destinationAmount', + type: 'uint256', + }, + ], + internalType: 'struct Pool.ReleaseOrMintOutV1', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.Config', + name: 'outboundConfig', + type: 'tuple', + }, + { + components: [ + { internalType: 'bool', name: 'isEnabled', type: 'bool' }, + { + internalType: 'uint128', + name: 'capacity', + type: 'uint128', + }, + { internalType: 'uint128', name: 'rate', type: 'uint128' }, + ], + internalType: 'struct RateLimiter.Config', + name: 'inboundConfig', + type: 'tuple', + }, + ], + name: 'setChainRateLimiterConfig', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'contract IPoolPriorTo1_5', + name: 'prevPool', + type: 'address', + }, + ], + name: 'setPreviousPool', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'rateLimitAdmin', + type: 'address', + }, + ], + name: 'setRateLimitAdmin', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'rebalancer', type: 'address' }], + name: 'setRebalancer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'remoteChainSelector', + type: 'uint64', + }, + { + internalType: 'bytes', + name: 'remotePoolAddress', + type: 'bytes', + }, + ], + name: 'setRemotePool', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'newRouter', type: 'address' }], + name: 'setRouter', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'transferLiquidity', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'to', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'typeAndVersion', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], + name: 'withdrawLiquidity', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/registry-module-owner-custom.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/registry-module-owner-custom.ts new file mode 100644 index 00000000..33745a3b --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/registry-module-owner-custom.ts @@ -0,0 +1,68 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v1_5_0/registry_module_owner_custom.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + inputs: [ + { + internalType: 'address', + name: 'tokenAdminRegistry', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'AddressZero', type: 'error' }, + { + inputs: [ + { internalType: 'address', name: 'admin', type: 'address' }, + { internalType: 'address', name: 'token', type: 'address' }, + ], + name: 'CanOnlySelfRegister', + type: 'error', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'token', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'administrator', + type: 'address', + }, + ], + name: 'AdministratorRegistered', + type: 'event', + }, + { + inputs: [{ internalType: 'address', name: 'token', type: 'address' }], + name: 'registerAdminViaGetCCIPAdmin', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'token', type: 'address' }], + name: 'registerAdminViaOwner', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'typeAndVersion', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/token-admin-registry.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/token-admin-registry.ts new file mode 100644 index 00000000..1e2ddcce --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/token-admin-registry.ts @@ -0,0 +1,335 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v1_5_0/token_admin_registry.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'function', + name: 'acceptAdminRole', + inputs: [{ name: 'localToken', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'acceptOwnership', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'addRegistryModule', + inputs: [{ name: 'module', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'getAllConfiguredTokens', + inputs: [ + { name: 'startIndex', type: 'uint64', internalType: 'uint64' }, + { name: 'maxCount', type: 'uint64', internalType: 'uint64' }, + ], + outputs: [{ name: 'tokens', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getPool', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getPools', + inputs: [{ name: 'tokens', type: 'address[]', internalType: 'address[]' }], + outputs: [{ name: '', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getTokenConfig', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + outputs: [ + { + name: '', + type: 'tuple', + internalType: 'struct TokenAdminRegistry.TokenConfig', + components: [ + { + name: 'administrator', + type: 'address', + internalType: 'address', + }, + { + name: 'pendingAdministrator', + type: 'address', + internalType: 'address', + }, + { + name: 'tokenPool', + type: 'address', + internalType: 'address', + }, + ], + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isAdministrator', + inputs: [ + { name: 'localToken', type: 'address', internalType: 'address' }, + { + name: 'administrator', + type: 'address', + internalType: 'address', + }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isRegistryModule', + inputs: [{ name: 'module', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'proposeAdministrator', + inputs: [ + { name: 'localToken', type: 'address', internalType: 'address' }, + { + name: 'administrator', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'removeRegistryModule', + inputs: [{ name: 'module', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setPool', + inputs: [ + { name: 'localToken', type: 'address', internalType: 'address' }, + { name: 'pool', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferAdminRole', + inputs: [ + { name: 'localToken', type: 'address', internalType: 'address' }, + { name: 'newAdmin', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'event', + name: 'AdministratorTransferRequested', + inputs: [ + { + name: 'token', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'currentAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'newAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'AdministratorTransferred', + inputs: [ + { + name: 'token', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'newAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + 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: 'PoolSet', + inputs: [ + { + name: 'token', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'previousPool', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'newPool', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RegistryModuleAdded', + inputs: [ + { + name: 'module', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RegistryModuleRemoved', + inputs: [ + { + name: 'module', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'error', + name: 'AlreadyRegistered', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + }, + { type: 'error', name: 'CannotTransferToSelf', inputs: [] }, + { + type: 'error', + name: 'InvalidTokenPoolToken', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + }, + { type: 'error', name: 'MustBeProposedOwner', inputs: [] }, + { + type: 'error', + name: 'OnlyAdministrator', + inputs: [ + { name: 'sender', type: 'address', internalType: 'address' }, + { name: 'token', type: 'address', internalType: 'address' }, + ], + }, + { type: 'error', name: 'OnlyCallableByOwner', inputs: [] }, + { + type: 'error', + name: 'OnlyPendingAdministrator', + inputs: [ + { name: 'sender', type: 'address', internalType: 'address' }, + { name: 'token', type: 'address', internalType: 'address' }, + ], + }, + { + type: 'error', + name: 'OnlyRegistryModuleOrOwner', + inputs: [{ name: 'sender', type: 'address', internalType: 'address' }], + }, + { type: 'error', name: 'OwnerCannotBeZero', inputs: [] }, + { type: 'error', name: 'ZeroAddress', inputs: [] }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/burn-mint-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/burn-mint-token-pool.ts new file mode 100644 index 00000000..f9be69bb --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/burn-mint-token-pool.ts @@ -0,0 +1,1171 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v1_5_1/burn_mint_token_pool.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { + name: 'token', + type: 'address', + internalType: 'contract IBurnMintERC20', + }, + { + name: 'localTokenDecimals', + type: 'uint8', + internalType: 'uint8', + }, + { + name: 'allowlist', + type: 'address[]', + internalType: 'address[]', + }, + { name: 'rmnProxy', type: 'address', internalType: 'address' }, + { name: 'router', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + 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: 'applyAllowListUpdates', + inputs: [ + { name: 'removes', type: 'address[]', internalType: 'address[]' }, + { name: 'adds', type: 'address[]', internalType: 'address[]' }, + ], + 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: 'getAllowList', + inputs: [], + outputs: [{ name: '', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getAllowListEnabled', + inputs: [], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCurrentInboundRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + outputs: [ + { + name: '', + 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: 'getCurrentOutboundRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + outputs: [ + { + name: '', + 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: 'getRateLimitAdmin', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + 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: 'getRmnProxy', + inputs: [], + outputs: [{ name: 'rmnProxy', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRouter', + inputs: [], + outputs: [{ name: 'router', 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: '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: '', + type: 'tuple', + internalType: 'struct Pool.LockOrBurnOutV1', + components: [ + { + name: 'destTokenAddress', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'destPoolData', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + ], + 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: 'amount', 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: 'removeRemotePool', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'remotePoolAddress', + type: 'bytes', + internalType: 'bytes', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setChainRateLimiterConfig', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'outboundConfig', + 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: 'inboundConfig', + 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: 'setChainRateLimiterConfigs', + inputs: [ + { + name: 'remoteChainSelectors', + type: 'uint64[]', + internalType: 'uint64[]', + }, + { + name: 'outboundConfigs', + 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: 'inboundConfigs', + 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: 'setRateLimitAdmin', + inputs: [ + { + name: 'rateLimitAdmin', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setRouter', + inputs: [{ name: 'newRouter', type: 'address', internalType: 'address' }], + 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: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'event', + name: 'AllowListAdd', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'AllowListRemove', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Burned', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + 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: 'ChainConfigured', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: false, + internalType: 'uint64', + }, + { + 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: 'ConfigChanged', + inputs: [ + { + name: 'config', + 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: 'Locked', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Minted', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'recipient', + type: 'address', + indexed: true, + 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: 'RateLimitAdminSet', + inputs: [ + { + name: 'rateLimitAdmin', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Released', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'recipient', + type: 'address', + indexed: true, + 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: 'RouterUpdated', + inputs: [ + { + name: 'oldRouter', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'newRouter', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'TokensConsumed', + inputs: [ + { + name: 'tokens', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'error', + name: 'AggregateValueMaxCapacityExceeded', + inputs: [ + { name: 'capacity', type: 'uint256', internalType: 'uint256' }, + { name: 'requested', type: 'uint256', internalType: 'uint256' }, + ], + }, + { + type: 'error', + name: 'AggregateValueRateLimitReached', + inputs: [ + { + name: 'minWaitInSeconds', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'available', type: 'uint256', internalType: 'uint256' }, + ], + }, + { type: 'error', name: 'AllowListNotEnabled', inputs: [] }, + { type: 'error', name: 'BucketOverfilled', inputs: [] }, + { + type: 'error', + name: 'CallerIsNotARampOnRouter', + 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: '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: '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: '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: 'RateLimitMustBeDisabled', inputs: [] }, + { + type: 'error', + name: 'SenderNotAllowed', + inputs: [{ name: 'sender', 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: 'ZeroAddressNotAllowed', inputs: [] }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/factory-burn-mint-erc20.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/factory-burn-mint-erc20.ts new file mode 100644 index 00000000..0dbf68f7 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/factory-burn-mint-erc20.ts @@ -0,0 +1,483 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v1_5_1/factory_burn_mint_erc20.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { name: 'name', type: 'string', internalType: 'string' }, + { name: 'symbol', type: 'string', internalType: 'string' }, + { name: 'decimals_', type: 'uint8', internalType: 'uint8' }, + { name: 'maxSupply_', type: 'uint256', internalType: 'uint256' }, + { name: 'preMint', type: 'uint256', internalType: 'uint256' }, + { name: 'newOwner', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'acceptOwnership', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'allowance', + inputs: [ + { name: 'owner', type: 'address', internalType: 'address' }, + { name: 'spender', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'approve', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'balanceOf', + inputs: [{ name: 'account', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'burn', + inputs: [{ name: 'amount', type: 'uint256', internalType: 'uint256' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'burn', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'burnFrom', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'decimals', + inputs: [], + outputs: [{ name: '', type: 'uint8', internalType: 'uint8' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'decreaseAllowance', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { + name: 'subtractedValue', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'decreaseApproval', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { + name: 'subtractedValue', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [{ name: 'success', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'getBurners', + inputs: [], + outputs: [{ name: '', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCCIPAdmin', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getMinters', + inputs: [], + outputs: [{ name: '', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'grantBurnRole', + inputs: [{ name: 'burner', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'grantMintAndBurnRoles', + inputs: [ + { + name: 'burnAndMinter', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'grantMintRole', + inputs: [{ name: 'minter', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'increaseAllowance', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'addedValue', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'increaseApproval', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'addedValue', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'isBurner', + inputs: [{ name: 'burner', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isMinter', + inputs: [{ name: 'minter', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'maxSupply', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'mint', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'name', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'revokeBurnRole', + inputs: [{ name: 'burner', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'revokeMintRole', + inputs: [{ name: 'minter', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setCCIPAdmin', + inputs: [{ name: 'newAdmin', type: 'address', internalType: 'address' }], + 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: 'symbol', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'totalSupply', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'transfer', + inputs: [ + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferFrom', + inputs: [ + { name: 'from', type: 'address', internalType: 'address' }, + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'Approval', + inputs: [ + { + name: 'owner', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'spender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'value', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'BurnAccessGranted', + inputs: [ + { + name: 'burner', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'BurnAccessRevoked', + inputs: [ + { + name: 'burner', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'CCIPAdminTransferred', + inputs: [ + { + name: 'previousAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'newAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'MintAccessGranted', + inputs: [ + { + name: 'minter', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'MintAccessRevoked', + inputs: [ + { + name: 'minter', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + 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: 'Transfer', + inputs: [ + { + name: 'from', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'to', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'value', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'CannotTransferToSelf', inputs: [] }, + { + type: 'error', + name: 'MaxSupplyExceeded', + inputs: [ + { + name: 'supplyAfterMint', + type: 'uint256', + internalType: 'uint256', + }, + ], + }, + { type: 'error', name: 'MustBeProposedOwner', inputs: [] }, + { type: 'error', name: 'OnlyCallableByOwner', inputs: [] }, + { type: 'error', name: 'OwnerCannotBeZero', inputs: [] }, + { + type: 'error', + name: 'SenderNotBurner', + inputs: [{ name: 'sender', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'SenderNotMinter', + inputs: [{ name: 'sender', type: 'address', internalType: 'address' }], + }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/lock-release-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/lock-release-token-pool.ts new file mode 100644 index 00000000..9b072f87 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/lock-release-token-pool.ts @@ -0,0 +1,1276 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v1_5_1/lock_release_token_pool.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { + name: 'token', + type: 'address', + internalType: 'contract IERC20', + }, + { + name: 'localTokenDecimals', + type: 'uint8', + internalType: 'uint8', + }, + { + name: 'allowlist', + type: 'address[]', + internalType: 'address[]', + }, + { name: 'rmnProxy', type: 'address', internalType: 'address' }, + { name: 'acceptLiquidity', type: 'bool', internalType: 'bool' }, + { name: 'router', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + 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: 'applyAllowListUpdates', + inputs: [ + { name: 'removes', type: 'address[]', internalType: 'address[]' }, + { name: 'adds', type: 'address[]', internalType: 'address[]' }, + ], + 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: 'canAcceptLiquidity', + inputs: [], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getAllowList', + inputs: [], + outputs: [{ name: '', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getAllowListEnabled', + inputs: [], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCurrentInboundRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + outputs: [ + { + name: '', + 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: 'getCurrentOutboundRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + outputs: [ + { + name: '', + 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: 'getRateLimitAdmin', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRebalancer', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + 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: 'getRmnProxy', + inputs: [], + outputs: [{ name: 'rmnProxy', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRouter', + inputs: [], + outputs: [{ name: 'router', 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: '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: '', + type: 'tuple', + internalType: 'struct Pool.LockOrBurnOutV1', + components: [ + { + name: 'destTokenAddress', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'destPoolData', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'provideLiquidity', + inputs: [{ name: 'amount', type: 'uint256', internalType: 'uint256' }], + outputs: [], + 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: 'amount', 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: 'removeRemotePool', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'remotePoolAddress', + type: 'bytes', + internalType: 'bytes', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setChainRateLimiterConfig', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'outboundConfig', + 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: 'inboundConfig', + 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: 'setChainRateLimiterConfigs', + inputs: [ + { + name: 'remoteChainSelectors', + type: 'uint64[]', + internalType: 'uint64[]', + }, + { + name: 'outboundConfigs', + 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: 'inboundConfigs', + 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: 'setRateLimitAdmin', + inputs: [ + { + name: 'rateLimitAdmin', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setRebalancer', + inputs: [{ name: 'rebalancer', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setRouter', + inputs: [{ name: 'newRouter', type: 'address', internalType: 'address' }], + 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: 'transferLiquidity', + inputs: [ + { name: 'from', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'withdrawLiquidity', + inputs: [{ name: 'amount', type: 'uint256', internalType: 'uint256' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'AllowListAdd', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'AllowListRemove', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Burned', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + 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: 'ChainConfigured', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: false, + internalType: 'uint64', + }, + { + 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: 'ConfigChanged', + inputs: [ + { + name: 'config', + 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: 'LiquidityAdded', + inputs: [ + { + name: 'provider', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: true, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'LiquidityRemoved', + inputs: [ + { + name: 'provider', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: true, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'LiquidityTransferred', + inputs: [ + { + name: 'from', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Locked', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Minted', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'recipient', + type: 'address', + indexed: true, + 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: 'RateLimitAdminSet', + inputs: [ + { + name: 'rateLimitAdmin', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Released', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'recipient', + type: 'address', + indexed: true, + 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: 'RouterUpdated', + inputs: [ + { + name: 'oldRouter', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'newRouter', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'TokensConsumed', + inputs: [ + { + name: 'tokens', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'error', + name: 'AggregateValueMaxCapacityExceeded', + inputs: [ + { name: 'capacity', type: 'uint256', internalType: 'uint256' }, + { name: 'requested', type: 'uint256', internalType: 'uint256' }, + ], + }, + { + type: 'error', + name: 'AggregateValueRateLimitReached', + inputs: [ + { + name: 'minWaitInSeconds', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'available', type: 'uint256', internalType: 'uint256' }, + ], + }, + { type: 'error', name: 'AllowListNotEnabled', inputs: [] }, + { type: 'error', name: 'BucketOverfilled', inputs: [] }, + { + type: 'error', + name: 'CallerIsNotARampOnRouter', + 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: '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: 'InsufficientLiquidity', inputs: [] }, + { + type: 'error', + name: 'InvalidDecimalArgs', + inputs: [ + { name: 'expected', type: 'uint8', internalType: 'uint8' }, + { name: 'actual', type: 'uint8', internalType: 'uint8' }, + ], + }, + { + 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: 'LiquidityNotAccepted', inputs: [] }, + { 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: 'RateLimitMustBeDisabled', inputs: [] }, + { + type: 'error', + name: 'SenderNotAllowed', + inputs: [{ name: 'sender', 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: 'ZeroAddressNotAllowed', inputs: [] }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_0/registry-module-owner-custom.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_0/registry-module-owner-custom.ts new file mode 100644 index 00000000..ef36c111 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_0/registry-module-owner-custom.ts @@ -0,0 +1,84 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v1_6_0/registry_module_owner_custom.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { + name: 'tokenAdminRegistry', + type: 'address', + internalType: 'address', + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'registerAccessControlDefaultAdmin', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'registerAdminViaGetCCIPAdmin', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'registerAdminViaOwner', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'event', + name: 'AdministratorRegistered', + inputs: [ + { + name: 'token', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'administrator', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'AddressZero', inputs: [] }, + { + type: 'error', + name: 'CanOnlySelfRegister', + inputs: [ + { name: 'admin', type: 'address', internalType: 'address' }, + { name: 'token', type: 'address', internalType: 'address' }, + ], + }, + { + type: 'error', + name: 'RequiredRoleNotFound', + inputs: [ + { name: 'msgSender', type: 'address', internalType: 'address' }, + { name: 'role', type: 'bytes32', internalType: 'bytes32' }, + { name: 'token', type: 'address', internalType: 'address' }, + ], + }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_1/burn-mint-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_1/burn-mint-token-pool.ts new file mode 100644 index 00000000..a9243ee8 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_1/burn-mint-token-pool.ts @@ -0,0 +1,1171 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v1_6_1/burn_mint_token_pool.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { + name: 'token', + type: 'address', + internalType: 'contract IBurnMintERC20', + }, + { + name: 'localTokenDecimals', + type: 'uint8', + internalType: 'uint8', + }, + { + name: 'allowlist', + type: 'address[]', + internalType: 'address[]', + }, + { name: 'rmnProxy', type: 'address', internalType: 'address' }, + { name: 'router', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + 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: 'applyAllowListUpdates', + inputs: [ + { name: 'removes', type: 'address[]', internalType: 'address[]' }, + { name: 'adds', type: 'address[]', internalType: 'address[]' }, + ], + 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: 'getAllowList', + inputs: [], + outputs: [{ name: '', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getAllowListEnabled', + inputs: [], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCurrentInboundRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + outputs: [ + { + name: '', + 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: 'getCurrentOutboundRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + outputs: [ + { + name: '', + 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: 'getRateLimitAdmin', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + 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: 'getRmnProxy', + inputs: [], + outputs: [{ name: 'rmnProxy', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRouter', + inputs: [], + outputs: [{ name: 'router', 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: '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: '', + type: 'tuple', + internalType: 'struct Pool.LockOrBurnOutV1', + components: [ + { + name: 'destTokenAddress', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'destPoolData', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + ], + 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: 'removeRemotePool', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'remotePoolAddress', + type: 'bytes', + internalType: 'bytes', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setChainRateLimiterConfig', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'outboundConfig', + 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: 'inboundConfig', + 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: 'setChainRateLimiterConfigs', + inputs: [ + { + name: 'remoteChainSelectors', + type: 'uint64[]', + internalType: 'uint64[]', + }, + { + name: 'outboundConfigs', + 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: 'inboundConfigs', + 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: 'setRateLimitAdmin', + inputs: [ + { + name: 'rateLimitAdmin', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setRouter', + inputs: [{ name: 'newRouter', type: 'address', internalType: 'address' }], + 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: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'event', + name: 'AllowListAdd', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'AllowListRemove', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + 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: 'ChainConfigured', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: false, + internalType: 'uint64', + }, + { + 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: 'ConfigChanged', + inputs: [ + { + name: 'config', + 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: '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: '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: 'RateLimitAdminSet', + inputs: [ + { + name: 'rateLimitAdmin', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + 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: 'RouterUpdated', + inputs: [ + { + name: 'oldRouter', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'newRouter', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'AllowListNotEnabled', inputs: [] }, + { type: 'error', name: 'BucketOverfilled', inputs: [] }, + { + type: 'error', + name: 'CallerIsNotARampOnRouter', + 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: '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: '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: '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: 'SenderNotAllowed', + inputs: [{ name: 'sender', 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: 'ZeroAddressNotAllowed', inputs: [] }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_1/lock-release-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_1/lock-release-token-pool.ts new file mode 100644 index 00000000..8863f833 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_1/lock-release-token-pool.ts @@ -0,0 +1,1286 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v1_6_1/lock_release_token_pool.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { + name: 'token', + type: 'address', + internalType: 'contract IERC20', + }, + { + name: 'localTokenDecimals', + type: 'uint8', + internalType: 'uint8', + }, + { + name: 'allowlist', + type: 'address[]', + internalType: 'address[]', + }, + { name: 'rmnProxy', type: 'address', internalType: 'address' }, + { name: 'router', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + 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: 'applyAllowListUpdates', + inputs: [ + { name: 'removes', type: 'address[]', internalType: 'address[]' }, + { name: 'adds', type: 'address[]', internalType: 'address[]' }, + ], + 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: 'getAllowList', + inputs: [], + outputs: [{ name: '', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getAllowListEnabled', + inputs: [], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCurrentInboundRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + outputs: [ + { + name: '', + 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: 'getCurrentOutboundRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + ], + outputs: [ + { + name: '', + 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: 'getRateLimitAdmin', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRebalancer', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + 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: 'getRmnProxy', + inputs: [], + outputs: [{ name: 'rmnProxy', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRouter', + inputs: [], + outputs: [{ name: 'router', 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: '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: '', + type: 'tuple', + internalType: 'struct Pool.LockOrBurnOutV1', + components: [ + { + name: 'destTokenAddress', + type: 'bytes', + internalType: 'bytes', + }, + { + name: 'destPoolData', + type: 'bytes', + internalType: 'bytes', + }, + ], + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'provideLiquidity', + inputs: [{ name: 'amount', type: 'uint256', internalType: 'uint256' }], + outputs: [], + 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', + }, + ], + }, + ], + 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: 'setChainRateLimiterConfig', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { + name: 'outboundConfig', + 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: 'inboundConfig', + 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: 'setChainRateLimiterConfigs', + inputs: [ + { + name: 'remoteChainSelectors', + type: 'uint64[]', + internalType: 'uint64[]', + }, + { + name: 'outboundConfigs', + 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: 'inboundConfigs', + 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: 'setRateLimitAdmin', + inputs: [ + { + name: 'rateLimitAdmin', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setRebalancer', + inputs: [{ name: 'rebalancer', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setRouter', + inputs: [{ name: 'newRouter', type: 'address', internalType: 'address' }], + 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: 'transferLiquidity', + inputs: [ + { name: 'from', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'withdrawLiquidity', + inputs: [{ name: 'amount', type: 'uint256', internalType: 'uint256' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'AllowListAdd', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'AllowListRemove', + inputs: [ + { + name: 'sender', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + 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: 'ChainConfigured', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + indexed: false, + internalType: 'uint64', + }, + { + 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: 'ConfigChanged', + inputs: [ + { + name: 'config', + 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: '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: 'LiquidityAdded', + inputs: [ + { + name: 'provider', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: true, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'LiquidityRemoved', + inputs: [ + { + name: 'provider', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: true, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'LiquidityTransferred', + inputs: [ + { + name: 'from', + type: 'address', + indexed: true, + 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: '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: 'RateLimitAdminSet', + inputs: [ + { + name: 'rateLimitAdmin', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RebalancerSet', + inputs: [ + { + name: 'oldRebalancer', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'newRebalancer', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + 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: 'RouterUpdated', + inputs: [ + { + name: 'oldRouter', + type: 'address', + indexed: false, + internalType: 'address', + }, + { + name: 'newRouter', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'AllowListNotEnabled', inputs: [] }, + { type: 'error', name: 'BucketOverfilled', inputs: [] }, + { + type: 'error', + name: 'CallerIsNotARampOnRouter', + 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: '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: 'InsufficientLiquidity', inputs: [] }, + { + type: 'error', + name: 'InvalidDecimalArgs', + inputs: [ + { name: 'expected', type: 'uint8', internalType: 'uint8' }, + { name: 'actual', type: 'uint8', internalType: 'uint8' }, + ], + }, + { + 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: '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: 'SenderNotAllowed', + inputs: [{ name: 'sender', 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: 'ZeroAddressNotAllowed', inputs: [] }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_2/factory-burn-mint-erc20.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_2/factory-burn-mint-erc20.ts new file mode 100644 index 00000000..5df804e9 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V1_6_2/factory-burn-mint-erc20.ts @@ -0,0 +1,490 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v1_6_2/factory_burn_mint_erc20.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { name: 'name', type: 'string', internalType: 'string' }, + { name: 'symbol', type: 'string', internalType: 'string' }, + { name: 'decimals_', type: 'uint8', internalType: 'uint8' }, + { name: 'maxSupply_', type: 'uint256', internalType: 'uint256' }, + { name: 'preMint', type: 'uint256', internalType: 'uint256' }, + { name: 'newOwner', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'acceptOwnership', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'allowance', + inputs: [ + { name: 'owner', type: 'address', internalType: 'address' }, + { name: 'spender', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'approve', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'balanceOf', + inputs: [{ name: 'account', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'burn', + inputs: [{ name: 'amount', type: 'uint256', internalType: 'uint256' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'burn', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'burnFrom', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'decimals', + inputs: [], + outputs: [{ name: '', type: 'uint8', internalType: 'uint8' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'decreaseAllowance', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { + name: 'subtractedValue', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'decreaseApproval', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { + name: 'subtractedValue', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [{ name: 'success', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'getBurners', + inputs: [], + outputs: [{ name: '', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCCIPAdmin', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getMinters', + inputs: [], + outputs: [{ name: '', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'grantBurnRole', + inputs: [{ name: 'burner', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'grantMintAndBurnRoles', + inputs: [ + { + name: 'burnAndMinter', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'grantMintRole', + inputs: [{ name: 'minter', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'increaseAllowance', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'addedValue', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'increaseApproval', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'addedValue', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'isBurner', + inputs: [{ name: 'burner', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isMinter', + inputs: [{ name: 'minter', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'maxSupply', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'mint', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'name', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'revokeBurnRole', + inputs: [{ name: 'burner', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'revokeMintRole', + inputs: [{ name: 'minter', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setCCIPAdmin', + inputs: [{ name: 'newAdmin', type: 'address', internalType: 'address' }], + 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: 'symbol', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'totalSupply', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'transfer', + inputs: [ + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferFrom', + inputs: [ + { name: 'from', type: 'address', internalType: 'address' }, + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'pure', + }, + { + type: 'event', + name: 'Approval', + inputs: [ + { + name: 'owner', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'spender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'value', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'BurnAccessGranted', + inputs: [ + { + name: 'burner', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'BurnAccessRevoked', + inputs: [ + { + name: 'burner', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'CCIPAdminTransferred', + inputs: [ + { + name: 'previousAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'newAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'MintAccessGranted', + inputs: [ + { + name: 'minter', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'MintAccessRevoked', + inputs: [ + { + name: 'minter', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + 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: 'Transfer', + inputs: [ + { + name: 'from', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'to', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'value', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'CannotTransferToSelf', inputs: [] }, + { + type: 'error', + name: 'MaxSupplyExceeded', + inputs: [ + { + name: 'supplyAfterMint', + type: 'uint256', + internalType: 'uint256', + }, + ], + }, + { type: 'error', name: 'MustBeProposedOwner', inputs: [] }, + { type: 'error', name: 'OnlyCallableByOwner', inputs: [] }, + { type: 'error', name: 'OwnerCannotBeZero', inputs: [] }, + { + type: 'error', + name: 'SenderNotBurner', + inputs: [{ name: 'sender', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'SenderNotMinter', + inputs: [{ name: 'sender', type: 'address', internalType: 'address' }], + }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/burn-from-mint-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/burn-from-mint-token-pool.ts new file mode 100644 index 00000000..e945540d --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/burn-from-mint-token-pool.ts @@ -0,0 +1,1665 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v2_0_0/burn_from_mint_token_pool.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { + name: 'token', + type: 'address', + internalType: 'contract IBurnMintERC20', + }, + { + name: 'localTokenDecimals', + type: 'uint8', + internalType: 'uint8', + }, + { + name: 'advancedPoolHooks', + type: 'address', + internalType: 'address', + }, + { name: 'rmnProxy', type: 'address', internalType: 'address' }, + { name: 'router', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: 'getAllowedFinalityConfig', + inputs: [], + outputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCurrentRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { name: 'fastFinality', 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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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: '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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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: 'bytes4', internalType: 'bytes4' }, + { 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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', + }, + ], + }, + { + name: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + 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', + }, + ], + }, + ], + 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: 'setAllowedFinalityConfig', + inputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + 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: 'setRateLimitConfig', + inputs: [ + { + name: 'rateLimitConfigArgs', + type: 'tuple[]', + internalType: 'struct TokenPool.RateLimitConfigArgs[]', + components: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { name: 'fastFinality', 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: 'view', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'pure', + }, + { + 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: '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: 'FastFinalityInboundRateLimitConsumed', + 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: 'FastFinalityOutboundRateLimitConsumed', + 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: '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: 'FinalityConfigSet', + inputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + indexed: false, + internalType: 'bytes4', + }, + ], + 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: '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: 'fastFinality', + 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: '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: '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: 'InvalidRequestedFinality', + inputs: [ + { + name: 'requestedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + }, + { + 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: '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: 'RequestedFinalityCanOnlyHaveOneMode', + inputs: [ + { + name: 'encodedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + }, + { + 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/cct/evm/artifacts/abi/V2_0_0/burn-mint-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/burn-mint-token-pool.ts new file mode 100644 index 00000000..ab3df104 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/burn-mint-token-pool.ts @@ -0,0 +1,1665 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v2_0_0/burn_mint_token_pool.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { + name: 'token', + type: 'address', + internalType: 'contract IBurnMintERC20', + }, + { + name: 'localTokenDecimals', + type: 'uint8', + internalType: 'uint8', + }, + { + name: 'advancedPoolHooks', + type: 'address', + internalType: 'address', + }, + { name: 'rmnProxy', type: 'address', internalType: 'address' }, + { name: 'router', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: 'getAllowedFinalityConfig', + inputs: [], + outputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCurrentRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { name: 'fastFinality', 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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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: '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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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: 'bytes4', internalType: 'bytes4' }, + { 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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', + }, + ], + }, + { + name: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + 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', + }, + ], + }, + ], + 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: 'setAllowedFinalityConfig', + inputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + 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: 'setRateLimitConfig', + inputs: [ + { + name: 'rateLimitConfigArgs', + type: 'tuple[]', + internalType: 'struct TokenPool.RateLimitConfigArgs[]', + components: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { name: 'fastFinality', 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: 'view', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'pure', + }, + { + 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: '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: 'FastFinalityInboundRateLimitConsumed', + 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: 'FastFinalityOutboundRateLimitConsumed', + 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: '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: 'FinalityConfigSet', + inputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + indexed: false, + internalType: 'bytes4', + }, + ], + 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: '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: 'fastFinality', + 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: '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: '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: 'InvalidRequestedFinality', + inputs: [ + { + name: 'requestedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + }, + { + 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: '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: 'RequestedFinalityCanOnlyHaveOneMode', + inputs: [ + { + name: 'encodedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + }, + { + 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/cct/evm/artifacts/abi/V2_0_0/burn-with-from-mint-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/burn-with-from-mint-token-pool.ts new file mode 100644 index 00000000..b5cfe27d --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/burn-with-from-mint-token-pool.ts @@ -0,0 +1,1665 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v2_0_0/burn_with_from_mint_token_pool.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { + name: 'token', + type: 'address', + internalType: 'contract IBurnMintERC20', + }, + { + name: 'localTokenDecimals', + type: 'uint8', + internalType: 'uint8', + }, + { + name: 'advancedPoolHooks', + type: 'address', + internalType: 'address', + }, + { name: 'rmnProxy', type: 'address', internalType: 'address' }, + { name: 'router', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: 'getAllowedFinalityConfig', + inputs: [], + outputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCurrentRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { name: 'fastFinality', 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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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: '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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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: 'bytes4', internalType: 'bytes4' }, + { 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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', + }, + ], + }, + { + name: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + 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', + }, + ], + }, + ], + 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: 'setAllowedFinalityConfig', + inputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + 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: 'setRateLimitConfig', + inputs: [ + { + name: 'rateLimitConfigArgs', + type: 'tuple[]', + internalType: 'struct TokenPool.RateLimitConfigArgs[]', + components: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { name: 'fastFinality', 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: 'view', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'pure', + }, + { + 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: '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: 'FastFinalityInboundRateLimitConsumed', + 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: 'FastFinalityOutboundRateLimitConsumed', + 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: '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: 'FinalityConfigSet', + inputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + indexed: false, + internalType: 'bytes4', + }, + ], + 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: '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: 'fastFinality', + 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: '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: '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: 'InvalidRequestedFinality', + inputs: [ + { + name: 'requestedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + }, + { + 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: '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: 'RequestedFinalityCanOnlyHaveOneMode', + inputs: [ + { + name: 'encodedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + }, + { + 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/cct/evm/artifacts/abi/V2_0_0/cross-chain-token.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/cross-chain-token.ts new file mode 100644 index 00000000..e967eebf --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/cross-chain-token.ts @@ -0,0 +1,661 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v2_0_0/cross_chain_token.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { + name: 'args', + type: 'tuple', + internalType: 'struct BaseERC20.ConstructorParams', + components: [ + { name: 'name', type: 'string', internalType: 'string' }, + { name: 'symbol', type: 'string', internalType: 'string' }, + { + name: 'maxSupply', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'preMint', type: 'uint256', internalType: 'uint256' }, + { + name: 'preMintRecipient', + type: 'address', + internalType: 'address', + }, + { name: 'decimals', type: 'uint8', internalType: 'uint8' }, + { + name: 'ccipAdmin', + type: 'address', + internalType: 'address', + }, + ], + }, + { + name: 'burnMintRoleAdmin', + type: 'address', + internalType: 'address', + }, + { name: 'owner', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'BURNER_ROLE', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'BURN_MINT_ADMIN_ROLE', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'DEFAULT_ADMIN_ROLE', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'MINTER_ROLE', + inputs: [], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'acceptDefaultAdminTransfer', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'allowance', + inputs: [ + { name: 'owner', type: 'address', internalType: 'address' }, + { name: 'spender', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'approve', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'balanceOf', + inputs: [{ name: 'account', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'beginDefaultAdminTransfer', + inputs: [{ name: 'newAdmin', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'burn', + inputs: [{ name: 'amount', type: 'uint256', internalType: 'uint256' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'burn', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'burnFrom', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'cancelDefaultAdminTransfer', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'changeDefaultAdminDelay', + inputs: [{ name: 'newDelay', type: 'uint48', internalType: 'uint48' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'decimals', + inputs: [], + outputs: [{ name: '_decimals', type: 'uint8', internalType: 'uint8' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'defaultAdmin', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'defaultAdminDelay', + inputs: [], + outputs: [{ name: '', type: 'uint48', internalType: 'uint48' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'defaultAdminDelayIncreaseWait', + inputs: [], + outputs: [{ name: '', type: 'uint48', internalType: 'uint48' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCCIPAdmin', + inputs: [], + outputs: [{ name: 'ccipAdmin', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getRoleAdmin', + inputs: [{ name: 'role', type: 'bytes32', internalType: 'bytes32' }], + outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'grantMintAndBurnRoles', + inputs: [ + { + name: 'burnAndMinter', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'grantRole', + inputs: [ + { name: 'role', type: 'bytes32', internalType: 'bytes32' }, + { name: 'account', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'hasRole', + inputs: [ + { name: 'role', type: 'bytes32', internalType: 'bytes32' }, + { name: 'account', type: 'address', internalType: 'address' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'maxSupply', + inputs: [], + outputs: [{ name: '_maxSupply', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'mint', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'name', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'pendingDefaultAdmin', + inputs: [], + outputs: [ + { name: 'newAdmin', type: 'address', internalType: 'address' }, + { name: 'schedule', type: 'uint48', internalType: 'uint48' }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'pendingDefaultAdminDelay', + inputs: [], + outputs: [ + { name: 'newDelay', type: 'uint48', internalType: 'uint48' }, + { name: 'schedule', type: 'uint48', internalType: 'uint48' }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'renounceRole', + inputs: [ + { name: 'role', type: 'bytes32', internalType: 'bytes32' }, + { name: 'account', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'revokeRole', + inputs: [ + { name: 'role', type: 'bytes32', internalType: 'bytes32' }, + { name: 'account', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'rollbackDefaultAdminDelay', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setCCIPAdmin', + inputs: [{ name: 'newAdmin', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'supportsInterface', + inputs: [{ name: 'interfaceId', type: 'bytes4', internalType: 'bytes4' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'symbol', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'totalSupply', + inputs: [], + outputs: [{ name: '', type: 'uint256', internalType: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'transfer', + inputs: [ + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferFrom', + inputs: [ + { name: 'from', type: 'address', internalType: 'address' }, + { name: 'to', type: 'address', internalType: 'address' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'pure', + }, + { + type: 'event', + name: 'Approval', + inputs: [ + { + name: 'owner', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'spender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'value', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'CCIPAdminTransferred', + inputs: [ + { + name: 'previousAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'newAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'DefaultAdminDelayChangeCanceled', + inputs: [], + anonymous: false, + }, + { + type: 'event', + name: 'DefaultAdminDelayChangeScheduled', + inputs: [ + { + name: 'newDelay', + type: 'uint48', + indexed: false, + internalType: 'uint48', + }, + { + name: 'effectSchedule', + type: 'uint48', + indexed: false, + internalType: 'uint48', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'DefaultAdminTransferCanceled', + inputs: [], + anonymous: false, + }, + { + type: 'event', + name: 'DefaultAdminTransferScheduled', + inputs: [ + { + name: 'newAdmin', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'acceptSchedule', + type: 'uint48', + indexed: false, + internalType: 'uint48', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RoleAdminChanged', + inputs: [ + { + name: 'role', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', + }, + { + name: 'previousAdminRole', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', + }, + { + name: 'newAdminRole', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RoleGranted', + inputs: [ + { + name: 'role', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', + }, + { + name: 'account', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'RoleRevoked', + inputs: [ + { + name: 'role', + type: 'bytes32', + indexed: true, + internalType: 'bytes32', + }, + { + name: 'account', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'sender', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Transfer', + inputs: [ + { + name: 'from', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'to', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'value', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'AccessControlBadConfirmation', inputs: [] }, + { + type: 'error', + name: 'AccessControlEnforcedDefaultAdminDelay', + inputs: [{ name: 'schedule', type: 'uint48', internalType: 'uint48' }], + }, + { + type: 'error', + name: 'AccessControlEnforcedDefaultAdminRules', + inputs: [], + }, + { + type: 'error', + name: 'AccessControlInvalidDefaultAdmin', + inputs: [ + { + name: 'defaultAdmin', + type: 'address', + internalType: 'address', + }, + ], + }, + { + type: 'error', + name: 'AccessControlUnauthorizedAccount', + inputs: [ + { name: 'account', type: 'address', internalType: 'address' }, + { name: 'neededRole', type: 'bytes32', internalType: 'bytes32' }, + ], + }, + { type: 'error', name: 'CannotRenounceCCIPAdmin', inputs: [] }, + { + type: 'error', + name: 'ERC20InsufficientAllowance', + inputs: [ + { name: 'spender', type: 'address', internalType: 'address' }, + { name: 'allowance', type: 'uint256', internalType: 'uint256' }, + { name: 'needed', type: 'uint256', internalType: 'uint256' }, + ], + }, + { + type: 'error', + name: 'ERC20InsufficientBalance', + inputs: [ + { name: 'sender', type: 'address', internalType: 'address' }, + { name: 'balance', type: 'uint256', internalType: 'uint256' }, + { name: 'needed', type: 'uint256', internalType: 'uint256' }, + ], + }, + { + type: 'error', + name: 'ERC20InvalidApprover', + inputs: [{ name: 'approver', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'ERC20InvalidReceiver', + inputs: [{ name: 'receiver', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'ERC20InvalidSender', + inputs: [{ name: 'sender', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'ERC20InvalidSpender', + inputs: [{ name: 'spender', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'MaxSupplyExceeded', + inputs: [ + { + name: 'supplyAfterMint', + type: 'uint256', + internalType: 'uint256', + }, + { name: 'maxSupply', type: 'uint256', internalType: 'uint256' }, + ], + }, + { type: 'error', name: 'OnlyCCIPAdmin', inputs: [] }, + { type: 'error', name: 'PreMintAddressNotSet', inputs: [] }, + { + type: 'error', + name: 'PreMintRecipientSetWithZeroPreMint', + inputs: [ + { + name: 'preMintRecipient', + type: 'address', + internalType: 'address', + }, + ], + }, + { + type: 'error', + name: 'SafeCastOverflowedUintDowncast', + inputs: [ + { name: 'bits', type: 'uint8', internalType: 'uint8' }, + { name: 'value', type: 'uint256', internalType: 'uint256' }, + ], + }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/erc20-lockbox.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/erc20-lockbox.ts new file mode 100644 index 00000000..b498bff8 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/erc20-lockbox.ts @@ -0,0 +1,254 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v2_0_0/erc20_lock_box.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'acceptOwnership', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'applyAuthorizedCallerUpdates', + inputs: [ + { + name: 'authorizedCallerArgs', + type: 'tuple', + internalType: 'struct AuthorizedCallers.AuthorizedCallerArgs', + components: [ + { + name: 'addedCallers', + type: 'address[]', + internalType: 'address[]', + }, + { + name: 'removedCallers', + type: 'address[]', + internalType: 'address[]', + }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'deposit', + inputs: [ + { name: 'token', type: 'address', internalType: 'address' }, + { name: '', type: 'uint64', internalType: 'uint64' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'getAllAuthorizedCallers', + inputs: [], + outputs: [{ name: '', type: 'address[]', internalType: 'address[]' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getToken', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'contract IERC20' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'isTokenSupported', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + outputs: [{ name: '', type: 'bool', internalType: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + stateMutability: 'view', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'pure', + }, + { + type: 'function', + name: 'withdraw', + inputs: [ + { name: 'token', type: 'address', internalType: 'address' }, + { name: '', type: 'uint64', internalType: 'uint64' }, + { name: 'amount', type: 'uint256', internalType: 'uint256' }, + { name: 'recipient', type: 'address', internalType: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'AuthorizedCallerAdded', + inputs: [ + { + name: 'caller', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'AuthorizedCallerRemoved', + inputs: [ + { + name: 'caller', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Deposit', + inputs: [ + { + name: 'token', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'depositor', + type: 'address', + indexed: true, + 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: 'Withdrawal', + inputs: [ + { + name: 'token', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'recipient', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { type: 'error', name: 'CannotTransferToSelf', inputs: [] }, + { + type: 'error', + name: 'InsufficientBalance', + inputs: [ + { name: 'requested', type: 'uint256', internalType: 'uint256' }, + { name: 'available', type: 'uint256', internalType: 'uint256' }, + ], + }, + { type: 'error', name: 'MustBeProposedOwner', inputs: [] }, + { type: 'error', name: 'OnlyCallableByOwner', inputs: [] }, + { type: 'error', name: 'OwnerCannotBeZero', inputs: [] }, + { type: 'error', name: 'RecipientCannotBeZeroAddress', inputs: [] }, + { + type: 'error', + name: 'SafeERC20FailedOperation', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + }, + { type: 'error', name: 'TokenAmountCannotBeZero', inputs: [] }, + { + type: 'error', + name: 'UnauthorizedCaller', + inputs: [{ name: 'caller', type: 'address', internalType: 'address' }], + }, + { + type: 'error', + name: 'UnsupportedToken', + inputs: [{ name: 'token', type: 'address', internalType: 'address' }], + }, + { type: 'error', name: 'ZeroAddressNotAllowed', inputs: [] }, + // generate:end +] as const diff --git a/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/lock-release-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/lock-release-token-pool.ts new file mode 100644 index 00000000..1b888ccd --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/lock-release-token-pool.ts @@ -0,0 +1,1673 @@ +export default [ + // generate: + // (() => { + // const abi = require('@chainlink/contracts-ccip/abi/v2_0_0/lock_release_token_pool.json') + // return require('util').inspect(Array.isArray(abi) ? abi : abi.abi, { depth: 99 }).split('\n').slice(1, -1) + // })() + { + type: 'constructor', + inputs: [ + { + name: 'token', + type: 'address', + internalType: 'contract IERC20', + }, + { + name: 'localTokenDecimals', + type: 'uint8', + internalType: 'uint8', + }, + { + name: 'advancedPoolHooks', + type: 'address', + internalType: 'address', + }, + { name: 'rmnProxy', type: 'address', internalType: 'address' }, + { name: 'router', type: 'address', internalType: 'address' }, + { name: 'lockBox', type: 'address', internalType: 'address' }, + ], + stateMutability: 'nonpayable', + }, + { + 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: 'getAllowedFinalityConfig', + inputs: [], + outputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getCurrentRateLimiterState', + inputs: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { name: 'fastFinality', 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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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: 'getLockBox', + inputs: [], + outputs: [{ name: '', type: 'address', internalType: 'address' }], + 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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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: 'bytes4', internalType: 'bytes4' }, + { 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + { 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', + }, + ], + }, + { + name: 'requestedFinalityConfig', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + 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', + }, + ], + }, + ], + 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: 'setAllowedFinalityConfig', + inputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + 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: 'setRateLimitConfig', + inputs: [ + { + name: 'rateLimitConfigArgs', + type: 'tuple[]', + internalType: 'struct TokenPool.RateLimitConfigArgs[]', + components: [ + { + name: 'remoteChainSelector', + type: 'uint64', + internalType: 'uint64', + }, + { name: 'fastFinality', 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: 'view', + }, + { + type: 'function', + name: 'transferOwnership', + inputs: [{ name: 'to', type: 'address', internalType: 'address' }], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'typeAndVersion', + inputs: [], + outputs: [{ name: '', type: 'string', internalType: 'string' }], + stateMutability: 'pure', + }, + { + 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: '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: 'FastFinalityInboundRateLimitConsumed', + 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: 'FastFinalityOutboundRateLimitConsumed', + 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: '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: 'FinalityConfigSet', + inputs: [ + { + name: 'allowedFinality', + type: 'bytes4', + indexed: false, + internalType: 'bytes4', + }, + ], + 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: '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: 'fastFinality', + 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: 'finalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'fastFinalityFeeUSDCents', + type: 'uint32', + internalType: 'uint32', + }, + { + name: 'finalityTransferFeeBps', + type: 'uint16', + internalType: 'uint16', + }, + { + name: 'fastFinalityTransferFeeBps', + 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: '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: '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: 'InvalidRequestedFinality', + inputs: [ + { + name: 'requestedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + { + name: 'allowedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + }, + { + 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: '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: 'RequestedFinalityCanOnlyHaveOneMode', + inputs: [ + { + name: 'encodedFinality', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + }, + { + 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/cct/evm/artifacts/bytecode/V2_0_0/burn-from-mint-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/burn-from-mint-token-pool.ts new file mode 100644 index 00000000..d45c9142 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/burn-from-mint-token-pool.ts @@ -0,0 +1,4 @@ +export default // generate: +// `'${require('fs').readFileSync(require.resolve('@chainlink/contracts-ccip/bytecode/v2_0_0/burn_from_mint_token_pool.bin'), 'utf8').trim()}' as const` +'0x60e080604052346102aa5760a081615ee1803803809161001f82856102fb565b8339810103126102aa5780516001600160a01b03811691908290036102aa5761004a60208201610334565b61005660408301610342565b9061006f608061006860608601610342565b9401610342565b9233156102ea57600180546001600160a01b03191633179055841580156102d9575b80156102c8575b6102b7578460805260c052308403610220575b60a052600380546001600160a01b039283166001600160a01b0319918216179091556002805493909216921691909117905560405163095ea7b360e01b60208083019182523060248401526000196044808501919091528352906000906101136064856102fb565b83519082865af16000513d82610204575b5050156101bf575b604051615b2390816103be823960805181818161023e01528181610491015281816122700152818161244801528181612ab501528181612cb0015281816131a20152818161374f01526137a9015260a05181818161361501528181614928015281816149720152614ebc015260c0518181816102d9015281816113f50152818161230a01528181612b50015261323d0152f35b6101fd916101f860405163095ea7b360e01b602082015230602482015260006044820152604481526101f26064826102fb565b82610356565b610356565b388061012c565b9091506102185750813b15155b3880610124565b600114610211565b60405163313ce56760e01b8152602081600481885afa60009181610276575b5061024b575b506100ab565b60ff1660ff821681810361025f5750610245565b6332ad3e0760e11b60005260045260245260446000fd5b9091506020813d6020116102af575b81610292602093836102fb565b810103126102aa576102a390610334565b903861023f565b600080fd5b3d9150610285565b630a64406560e11b60005260046000fd5b506001600160a01b03811615610098565b506001600160a01b03841615610091565b639b15e16f60e01b60005260046000fd5b601f909101601f19168101906001600160401b0382119082101761031e57604052565b634e487b7160e01b600052604160045260246000fd5b519060ff821682036102aa57565b51906001600160a01b03821682036102aa57565b906000602091828151910182855af1156103b1576000513d6103a857506001600160a01b0381163b155b6103875750565b635274afe760e01b60009081526001600160a01b0391909116600452602490fd5b60011415610380565b6040513d6000823e3d90fdfe608080604052600436101561001357600080fd5b600090813560e01c90816301ffc9a7146139ca5750806306b859ef146138e5578063181f5a77146138845780631826b1e7146137cd57806321df0da71461377c578063240028e8146137185780632422ac451461363957806324f65ee7146135fb5780632cab0fb61461310757806337a3210d146130d35780633907753714612a0a5780634c5ef0ed146129c357806362ddd3c41461293c5780637437ff9f146128ee57806379ba5097146128275780638926f54f146127e15780638da5cb5b146127ad5780639a4575b9146121f7578063a42a7b8b14612090578063acfecf9114611f98578063ae39a25714611e0d578063b6cfa3b714611d52578063b794658014611d1a578063bfeffd3f14611c6e578063c4bffe2b14611b43578063c7230a601461189d578063dc04fa1f14611419578063dc0bd971146113c8578063dcbd41bc146111c4578063e8a1da1714610ae8578063ea6396db146109aa578063ec6ae7a714610967578063f2fde38b146108985763fbc801a71461019757600080fd5b346105db5760606003193601126105db576004359067ffffffffffffffff82116105db578160040160a060031984360301126105e9576101d5613afc565b9060443567ffffffffffffffff811161070f57906101fa610217923690600401613c27565b92906102046145e4565b5061020f8584615120565b933691613da1565b92608486019361022685614571565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361084e57602487019677ffffffffffffffff0000000000000000000000000000000061028c89614592565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156107c157889161081f575b506107f75767ffffffffffffffff61032089614592565b16610338816000526007602052604060002054151590565b156107cc57602073ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156107c1578890610770575b73ffffffffffffffffffffffffffffffffffffffff9150163303610744576064810135936103c78686613f88565b7fffffffff00000000000000000000000000000000000000000000000000000000851694851561072257610423907fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1690614a7c565b61043f816104308a614571565b6104398d614592565b90615408565b73ffffffffffffffffffffffffffffffffffffffff6003541693846105ed575b5050505050509061046f91613f88565b9161047984614592565b5073ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b156105e9576040517f79cc6790000000000000000000000000000000000000000000000000000000008152306004820152602481018590529082908290604490829084905af180156105de576105c6575b6105bc8461058b61058688877ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff61054c61054685614592565b93614571565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252336020830152810188905292169180606081015b0390a2614592565b614755565b90610594614eb5565b604051926105a184613d0c565b83526020830152604051928392604084526040840190613e69565b9060208301520390f35b6105d1828092613d60565b6105db5780610504565b80fd5b6040513d84823e3d90fd5b5080fd5b843b1561071e578994928b9694928692604051988997889687957fa8027c0f00000000000000000000000000000000000000000000000000000000875260048701608090528061063c91615372565b6084880160a0905261012488019061065392613fb6565b9261065d90613c12565b67ffffffffffffffff1660a487015260440161067890613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660c48601528d8c60e48701526106a390613bc3565b73ffffffffffffffffffffffffffffffffffffffff1661010486015260248501528381036003190160448501526106d991613c55565b90606483015203925af18015610713579085916106fa575b8080808061045f565b8161070491613d60565b61070f5783386106f1565b8380fd5b6040513d87823e3d90fd5b8980fd5b5061073f816107308a614571565b6107398d614592565b906153c2565b61043f565b6024877f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b506020813d6020116107b9575b8161078a60209383613d60565b810103126107b5576107b073ffffffffffffffffffffffffffffffffffffffff91613f95565b610399565b8780fd5b3d915061077d565b6040513d8a823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008852600452602487fd5b6004877f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b610841915060203d602011610847575b6108398183613d60565b810190614be8565b38610309565b503d61082f565b60248673ffffffffffffffffffffffffffffffffffffffff61086f88614571565b7f961c9a4f00000000000000000000000000000000000000000000000000000000835216600452fd5b50346105db5760206003193601126105db5773ffffffffffffffffffffffffffffffffffffffff6108c7613b5a565b6108cf614c00565b1633811461093f57807fffffffffffffffffffffffff000000000000000000000000000000000000000083541617825573ffffffffffffffffffffffffffffffffffffffff600154167fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12788380a380f35b6004827fdad89dca000000000000000000000000000000000000000000000000000000008152fd5b50346105db57806003193601126105db5760207fffffffff0000000000000000000000000000000000000000000000000000000060025460401b16604051908152f35b50346105db5760806003193601126105db576109c4613b5a565b506109cd613be4565b6109d5613b2b565b5060643567ffffffffffffffff8111610ae4579167ffffffffffffffff604092610a0560e0953690600401613c27565b50508260c08551610a1581613d44565b82815282602082015282878201528260608201528260808201528260a08201520152168152600b6020522060405190610a4d82613d44565b5461ffff818163ffffffff82169485815263ffffffff60208201818560201c1681528160408401818760401c168152816060860193818960601c16855260ff60c060808901988a8c60801c168a528a60a082019c60901c168c52019b60a01c1615158b526040519b8c52511660208b0152511660408901525116606087015251166080850152511660a083015251151560c0820152f35b8280fd5b50346105db5760406003193601126105db5760043567ffffffffffffffff81116105e957610b1a903690600401613e93565b9060243567ffffffffffffffff811161070f5790610b3d84923690600401613e93565b939091610b48614c00565b83905b8282106110055750505081927ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee182360301935b81811015611001578060051b83013585811215610ffd57830161012081360312610ffd5760405194610baf86613d28565b610bb882613c12565b8652602082013567ffffffffffffffff81116105e95782019436601f870112156105e957853595610be887613ef5565b96610bf66040519889613d60565b80885260208089019160051b83010190368211610ffd5760208301905b828210610fca575050505060208701958652604083013567ffffffffffffffff8111610ae457610c469036908501613e06565b9160408801928352610c70610c5e3660608701614801565b9460608a0195865260c0369101614801565b956080890196875283515115610fa257610c9467ffffffffffffffff8a51166157a5565b15610f6b5767ffffffffffffffff8951168252600860205260408220610cbb865182614ef0565b610cc9885160028301614ef0565b6004855191019080519067ffffffffffffffff8211610f3e57610cec8354614640565b601f8111610f03575b50602090601f8311600114610e6457610d439291869183610e59575b50507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b90555b815b88518051821015610d7d5790610d77600192610d708367ffffffffffffffff8f5116926145fd565b5190614c4b565b01610d48565b5050977f8d340f17e19058004c20453540862a9c62778504476f6756755cb33bcd6c38c2939199975095610e4b67ffffffffffffffff6001979694985116925193519151610e17610de260405196879687526101006020880152610100870190613c55565b9360408601906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b60a08401906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b0390a1019392909193610b7e565b015190508e80610d11565b83865281862091907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08416875b818110610eeb5750908460019594939210610eb4575b505050811b019055610d46565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690558d8080610ea7565b92936020600181928786015181550195019301610e91565b610f2e9084875260208720601f850160051c81019160208610610f34575b601f0160051c019061489d565b8d610cf5565b9091508190610f21565b6024857f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b60248267ffffffffffffffff8b51167f1d5ad3c5000000000000000000000000000000000000000000000000000000008252600452fd5b6004827f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b813567ffffffffffffffff8111610ff957602091610fee8392833691890101613e06565b815201910190610c13565b8680fd5b8480fd5b8380f35b9267ffffffffffffffff6110276110228486889a9699979a6147d4565b614592565b1691611032836154db565b1561119857828452600860205261104e60056040862001615478565b94845b865181101561108757600190858752600860205261108060056040892001611079838b6145fd565b5190615671565b5001611051565b50939692909450949094808752600860205260056040882088815588600182015588600282015588600382015588600482016110c38154614640565b80611157575b5050500180549088815581611139575b5050907f5204aec90a3c794d8e90fded8b46ae9c7c552803e7e832e0c1d358396d8599166020836001948a52600482528985604082208281550155808a52600582528985604082208281550155604051908152a101909194939294610b4b565b885260208820908101905b818110156110d957888155600101611144565b601f811160011461116d5750555b888a806110c9565b8183526020832061118891601f01861c81019060010161489d565b8082528160208120915555611165565b602484847f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b50346105db5760206003193601126105db5760043567ffffffffffffffff81116105e9576111f6903690600401613ec4565b73ffffffffffffffffffffffffffffffffffffffff600a5416331415806113a6575b61137a57825b818110611229578380f35b611234818385614777565b67ffffffffffffffff61124682614592565b169061125f826000526007602052604060002054151590565b1561134e57907f41f7c8f7cfdad9350aa495e6c54cbbf750a07ab38a9098aed1256e30dd1682bb60e08361130e6112e8602060019897018b6112a082614787565b156113155787905260046020526112c760408d206112c13660408801614801565b90614ef0565b868c5260056020526112e360408d206112c13660a08801614801565b614787565b9160405192151583526113016020840160408301614859565b60a0608084019101614859565ba20161121e565b60026040828a6112e3945260086020526113378282206112c136858c01614801565b8a8152600860205220016112c13660a08801614801565b602486837f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b6024837f8e4a23d600000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff60015416331415611218565b50346105db57806003193601126105db57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105db5760406003193601126105db5760043567ffffffffffffffff81116105e95761144b903690600401613ec4565b60243567ffffffffffffffff811161070f5761146b903690600401613e93565b919092611476614c00565b845b8281106114e257505050825b81811061148f578380f35b8067ffffffffffffffff6114a961102260019486886147d4565b16808652600b6020528560408120557f5479bbc0288b7eaeaf2ace0943b88016cc648964fcd42919a86fd93b15fdbee88680a201611484565b67ffffffffffffffff6114f9611022838686614777565b16611511816000526007602052604060002054151590565b1561187257611521828585614777565b602081019060e081019061153482614787565b156118465760a0810161271061ffff61154c83614794565b1610156118375760c082019161271061ffff61156785614794565b1610156117ff5763ffffffff61157c866147a3565b16156117d357858c52600b60205260408c20611597866147a3565b63ffffffff169080549060408401916115af836147a3565b60201b67ffffffff00000000169360608601946115cb866147a3565b60401b6bffffffff00000000000000001696608001966115ea886147a3565b60601b6fffffffff00000000000000000000000016916116098a614794565b60801b71ffff00000000000000000000000000000000169361162a8c614794565b60901b73ffff00000000000000000000000000000000000016957fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016177fffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffff16177fffffffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffff16177fffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffffffff1617171781556116dd87614787565b81547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff0000000000000000000000000000000000000000161790556040519661172e906147b4565b63ffffffff16875261173f906147b4565b63ffffffff166020870152611753906147b4565b63ffffffff166040860152611767906147b4565b63ffffffff16606085015261177b906147c5565b61ffff16608084015261178d906147c5565b61ffff1660a083015261179f90613cb4565b151560c082015260e07ffae1e296719dac5269c3886fb5002bb29bf17ae403060c6eb063a55abaaa104191a2600101611478565b60248c877f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b60248c61ffff61180e86614794565b7f95f3517a00000000000000000000000000000000000000000000000000000000835216600452fd5b8a61ffff61180e602493614794565b60248a857f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b7f1e670e4b000000000000000000000000000000000000000000000000000000008752600452602486fd5b50346105db5760406003193601126105db5760043567ffffffffffffffff81116105e9576118cf903690600401613e93565b906118d8613ba0565b9173ffffffffffffffffffffffffffffffffffffffff6001541633141580611b21575b611af55773ffffffffffffffffffffffffffffffffffffffff8316908115611acd57845b81811061192a578580f35b73ffffffffffffffffffffffffffffffffffffffff61195261194d8385886147d4565b614571565b166040517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152602081602481855afa9081156107c1578891611a9a575b50806119a7575b505060010161191f565b6040517fa9059cbb00000000000000000000000000000000000000000000000000000000602080830191825273ffffffffffffffffffffffffffffffffffffffff8a16602484015260448084018590528352918a9190611a08606482613d60565b519082865af115611a8f5787513d611a865750813b155b611a5a5790847f508d7d183612c18fc339b42618912b9fa3239f631dd7ec0671f950200a0fa66e602060019594604051908152a3903861199d565b602488837f5274afe7000000000000000000000000000000000000000000000000000000008252600452fd5b60011415611a1f565b6040513d89823e3d90fd5b905060203d8111611ac6575b611ab08183613d60565b602082600092810103126105db57505138611996565b503d611aa6565b6004857f8579befe000000000000000000000000000000000000000000000000000000008152fd5b6024847fcb1afbd700000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff600c54163314156118fb565b50346105db57806003193601126105db57604051906006548083528260208101600684526020842092845b818110611c55575050611b8392500383613d60565b8151611ba7611b9182613ef5565b91611b9f6040519384613d60565b808352613ef5565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0602083019301368437805b8451811015611c06578067ffffffffffffffff611bf3600193886145fd565b5116611bff82866145fd565b5201611bd4565b50925090604051928392602084019060208552518091526040840192915b818110611c32575050500390f35b825167ffffffffffffffff16845285945060209384019390920191600101611c24565b8454835260019485019487945060209093019201611b6e565b50346105db5760206003193601126105db5760043573ffffffffffffffffffffffffffffffffffffffff81168091036105e957611ca9614c00565b7fffffffffffffffffffffffff00000000000000000000000000000000000000006003547fbaff46844acf36d6ee996f489a1a288709c4542bd33cd557770afd267d5812096040805173ffffffffffffffffffffffffffffffffffffffff84168152856020820152a1161760035580f35b50346105db5760206003193601126105db57611d4e611d3a610586613bfb565b604051918291602083526020830190613c55565b0390f35b50346105db5760206003193601126105db577f307cf716eade81675bea3ccb6917b0f91baa2160056765d9a83d76f819caf06a6020611d8f613ac8565b611d97614c00565b6002547fffffffffffffffff00000000ffffffffffffffffffffffffffffffffffffffff77ffffffff0000000000000000000000000000000000000000808460401c16169116176002557fffffffff0000000000000000000000000000000000000000000000000000000060405191168152a180f35b50346105db5760606003193601126105db57611e27613b5a565b90611e30613ba0565b6044359273ffffffffffffffffffffffffffffffffffffffff841680850361070f57611e5a614c00565b73ffffffffffffffffffffffffffffffffffffffff82168015611f705794611f6a917f3f1036e85d016a93254a0b1415844f79b85424959d90ae5ad51ce8f4533fe70195967fffffffffffffffffffffffff0000000000000000000000000000000000000000600254161760025573ffffffffffffffffffffffffffffffffffffffff85167fffffffffffffffffffffffff0000000000000000000000000000000000000000600a541617600a557fffffffffffffffffffffffff0000000000000000000000000000000000000000600c541617600c556040519384938491604091949373ffffffffffffffffffffffffffffffffffffffff809281606087019816865216602085015216910152565b0390a180f35b6004857f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b50346105db5767ffffffffffffffff611fb036613e24565b929091611fbb614c00565b1691611fd4836000526007602052604060002054151590565b1561119857828452600860205261200360056040862001611ff6368486613da1565b6020815191012090615671565b1561204857907f52d00ee4d9bd51b40168f2afc5848837288ce258784ad914278791464b3f4d7691612042604051928392602084526020840191613fb6565b0390a280f35b8261208c836040519384937f74f23c7c0000000000000000000000000000000000000000000000000000000085526004850152604060248501526044840191613fb6565b0390fd5b50346105db5760206003193601126105db5767ffffffffffffffff6120b3613bfb565b16815260086020526120ca60056040832001615478565b80517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061210f6120f983613ef5565b926121076040519485613d60565b808452613ef5565b01835b8181106121e6575050825b82518110156121635780612133600192856145fd565b518552600960205261214760408620614693565b61215182856145fd565b5261215c81846145fd565b500161211d565b81846040519182916020830160208452825180915260408401602060408360051b870101940192905b82821061219b57505050500390f35b919360206121d6827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc060019597998495030186528851613c55565b960192019201859493919261218c565b806060602080938601015201612112565b50346105db5760206003193601126105db5760043567ffffffffffffffff81116105e957806004019060a06003198236030112610ae4576122366145e4565b506040516020936122478583613d60565b808252608483019161225883614571565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361278c57602484019477ffffffffffffffff000000000000000000000000000000006122be87614592565b60801b16604051907f2cbc26bb0000000000000000000000000000000000000000000000000000000082526004820152878160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa90811561271157849161276f575b506127475767ffffffffffffffff61235187614592565b16612369816000526007602052604060002054151590565b1561271c578773ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156127115784906126c9575b73ffffffffffffffffffffffffffffffffffffffff915016330361269d57606485013594612403866123fa87614571565b6107398a614592565b73ffffffffffffffffffffffffffffffffffffffff600354169182612580575b5050505061243084614592565b5073ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b156105e9576040517f79cc6790000000000000000000000000000000000000000000000000000000008152306004820152602481018590529082908290604490829084905af180156105de5761256b575b8561253b61058687877ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff8961057e6125046124fe87614592565b92614571565b6040805173ffffffffffffffffffffffffffffffffffffffff90921682523360208301528101959095529116929081906060820190565b90612544614eb5565b6040519261255184613d0c565b835281830152611d4e604051928284938452830190613e69565b612576828092613d60565b6105db57806124bb565b823b15610ffd57918791858094604051968795869485937fa8027c0f0000000000000000000000000000000000000000000000000000000085526004850160809052806125cc91615372565b6084860160a090526101248601906125e392613fb6565b916125ed90613c12565b67ffffffffffffffff1660a485015260440161260890613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660c48401528b60e48401526126328b613bc3565b73ffffffffffffffffffffffffffffffffffffffff1661010484015283602484015282810360031901604484015261266991613c55565b8a606483015203925af180156105de57908291612688575b8080612423565b8161269291613d60565b6105db578038612681565b6024837f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b508781813d831161270a575b6126df8183613d60565b8101031261070f5761270573ffffffffffffffffffffffffffffffffffffffff91613f95565b6123c9565b503d6126d5565b6040513d86823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008452600452602483fd5b6004837f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b6127869150883d8a11610847576108398183613d60565b3861233a565b5073ffffffffffffffffffffffffffffffffffffffff61086f602493614571565b50346105db57806003193601126105db57602073ffffffffffffffffffffffffffffffffffffffff60015416604051908152f35b50346105db5760206003193601126105db57602061281d67ffffffffffffffff612809613bfb565b166000526007602052604060002054151590565b6040519015158152f35b50346105db57806003193601126105db57805473ffffffffffffffffffffffffffffffffffffffff811633036128c6577fffffffffffffffffffffffff000000000000000000000000000000000000000060015491338284161760015516825573ffffffffffffffffffffffffffffffffffffffff3391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08380a380f35b6004827f02b543c6000000000000000000000000000000000000000000000000000000008152fd5b50346105db57806003193601126105db57600254600a54600c546040805173ffffffffffffffffffffffffffffffffffffffff94851681529284166020840152921691810191909152606090f35b50346105db5761294b36613e24565b61295793929193614c00565b67ffffffffffffffff8216612979816000526007602052604060002054151590565b156129985750612995929361298f913691613da1565b90614c4b565b80f35b7f1e670e4b000000000000000000000000000000000000000000000000000000008452600452602483fd5b50346105db5760406003193601126105db576129dd613bfb565b906024359067ffffffffffffffff82116105db57602061281d84612a043660048701613e06565b906145a7565b50346105db5760206003193601126105db576004359067ffffffffffffffff82116105db57816004019061010060031984360301126105db5780604051612a5081613cc1565b5280604051612a5e81613cc1565b52606483013560c4840193612a8e612a88612a83612a7c8888614520565b3691613da1565b6148b4565b8361496f565b936084820195612a9d87614571565b73ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116036130b257602483019377ffffffffffffffff00000000000000000000000000000000612b0386614592565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115611a8f578791613093575b5061306b5767ffffffffffffffff612b9786614592565b16612baf816000526007602052604060002054151590565b1561304057602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa908115611a8f578791613021575b5015612ff557612c2685614592565b92612c3c60a4860194612a04612a7c8785614520565b15612fae57612c5d88612c4e8b614571565b612c5789614592565b90615289565b73ffffffffffffffffffffffffffffffffffffffff600354169283612de0575b505050505060440191612c8f83614571565b612c9883614592565b5073ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001690813b15610ae4576040517f40c10f1900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff919091166004820152602481018690529082908290604490829084905af180156105de57612dcb575b5050608067ffffffffffffffff60209573ffffffffffffffffffffffffffffffffffffffff612d97612d916105467ffc5e3a5bddc11d92c2dc20fae6f7d5eb989f056be35239f7de7e86150609abc097614592565b96614571565b816040519716875233898801521660408601528560608601521692a260405190612dc082613cc1565b815260405190518152f35b612dd6828092613d60565b6105db5780612d3c565b833b156107b557878795938195938c93604051988997889687957f6371157400000000000000000000000000000000000000000000000000000000875260048701606090528d612e308780615372565b60648a0161010090526101648a0190612e4892613fb6565b94612e5290613c12565b67ffffffffffffffff166084890152604401612e6d90613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660a488015260c4870152612e9690613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660e4860152612ebb9084615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c86840301610104870152612ef09291613fb6565b90612efb9083615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c85840301610124860152612f309291613fb6565b9060e48a01612f3e91615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c84840301610144850152612f739291613fb6565b8b602483015282604483015203925af1801561271157908491612f99575b808080612c7d565b81612fa391613d60565b610ae4578238612f91565b83612fb891614520565b61208c6040519283927f24eb47e5000000000000000000000000000000000000000000000000000000008452602060048501526024840191613fb6565b6024867f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b61303a915060203d602011610847576108398183613d60565b38612c17565b7fa9902c7e000000000000000000000000000000000000000000000000000000008752600452602486fd5b6004867f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b6130ac915060203d602011610847576108398183613d60565b38612b80565b60248573ffffffffffffffffffffffffffffffffffffffff61086f8a614571565b50346105db57806003193601126105db57602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b50346105db5760406003193601126105db576004359067ffffffffffffffff82116105db57816004019061010060031984360301126105db57613148613afc565b918160405161315681613cc1565b5260648401359360c481019361317b613175612a83612a7c8887614520565b8761496f565b94608483019661318a88614571565b73ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116036135da57602484019477ffffffffffffffff000000000000000000000000000000006131f087614592565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156107c15788916135bb575b506107f75767ffffffffffffffff61328487614592565b1661329c816000526007602052604060002054151590565b156107cc57602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa9081156107c157889161359c575b50156107445761331386614592565b9361332960a4870195612a04612a7c8886614520565b15613592577fffffffff000000000000000000000000000000000000000000000000000000001690811561357757613373896133648c614571565b61336d8a614592565b90615302565b73ffffffffffffffffffffffffffffffffffffffff6003541693846133a6575b50505050505060440191612c8f83614571565b843b1561357357868995938c959387938b6040519a8b998a9889977f6371157400000000000000000000000000000000000000000000000000000000895260048901606090526133f68780615372565b60648b0161010090526101648b019061340e92613fb6565b9461341890613c12565b67ffffffffffffffff1660848a015260440161343390613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660a489015260c488015261345c90613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660e48701526134819084615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c878403016101048801526134b69291613fb6565b906134c19083615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c868403016101248701526134f69291613fb6565b9060e48b0161350491615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c858403016101448601526135399291613fb6565b908c6024840152604483015203925af180156127115761355e575b8080808080613393565b9261356c8160449395613d60565b9290613554565b8880fd5b61358d896135848c614571565b612c578a614592565b613373565b612fb88583614520565b6135b5915060203d602011610847576108398183613d60565b38613304565b6135d4915060203d602011610847576108398183613d60565b3861326d565b60248673ffffffffffffffffffffffffffffffffffffffff61086f8b614571565b50346105db57806003193601126105db57602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105db5760406003193601126105db57613653613bfb565b6024359182151583036105db57610140613716613670858561449d565b6136c660409392935180946fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565b60a08301906fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565bf35b50346105db5760206003193601126105db57602090613735613b5a565b905073ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116146040519015158152f35b50346105db57806003193601126105db57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105db5760c06003193601126105db576137e7613b5a565b506137f0613be4565b6137f8613b7d565b50608435917fffffffff00000000000000000000000000000000000000000000000000000000831683036105db5760a4359067ffffffffffffffff82116105db5760a063ffffffff8061ffff61385d88886138563660048b01613c27565b50506142ed565b95926040979194975197885216602087015216604085015216606083015215156080820152f35b50346105db57806003193601126105db5750611d4e6040516138a7604082613d60565b601b81527f4275726e46726f6d4d696e74546f6b656e506f6f6c20322e302e3000000000006020820152604051918291602083526020830190613c55565b50346105db5760c06003193601126105db576138ff613b5a565b613907613be4565b906064357fffffffff000000000000000000000000000000000000000000000000000000008116810361070f5760843567ffffffffffffffff8111610ffd57613954903690600401613c27565b9160a435936002851015610ff95761396f9560443591613ff5565b90604051918291602083016020845282518091526020604085019301915b81811061399b575050500390f35b825173ffffffffffffffffffffffffffffffffffffffff1684528594506020938401939092019160010161398d565b9050346105e95760206003193601126105e9576020907fffffffff00000000000000000000000000000000000000000000000000000000613a09613ac8565b167faff2afbf000000000000000000000000000000000000000000000000000000008114908115613a9e575b8115613a74575b8115613a4a575b5015158152f35b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501483613a43565b7f0e64dd290000000000000000000000000000000000000000000000000000000081149150613a3c565b7f940a15420000000000000000000000000000000000000000000000000000000081149150613a35565b600435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613af757565b600080fd5b602435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613af757565b604435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613af757565b6004359073ffffffffffffffffffffffffffffffffffffffff82168203613af757565b6064359073ffffffffffffffffffffffffffffffffffffffff82168203613af757565b6024359073ffffffffffffffffffffffffffffffffffffffff82168203613af757565b359073ffffffffffffffffffffffffffffffffffffffff82168203613af757565b6024359067ffffffffffffffff82168203613af757565b6004359067ffffffffffffffff82168203613af757565b359067ffffffffffffffff82168203613af757565b9181601f84011215613af75782359167ffffffffffffffff8311613af75760208381860195010111613af757565b919082519283825260005b848110613c9f5750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8460006020809697860101520116010190565b80602080928401015182828601015201613c60565b35908115158203613af757565b6020810190811067ffffffffffffffff821117613cdd57604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040810190811067ffffffffffffffff821117613cdd57604052565b60a0810190811067ffffffffffffffff821117613cdd57604052565b60e0810190811067ffffffffffffffff821117613cdd57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117613cdd57604052565b92919267ffffffffffffffff8211613cdd5760405191613de9601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200184613d60565b829481845281830111613af7578281602093846000960137010152565b9080601f83011215613af757816020613e2193359101613da1565b90565b906040600319830112613af75760043567ffffffffffffffff81168103613af757916024359067ffffffffffffffff8211613af757613e6591600401613c27565b9091565b613e21916020613e828351604084526040840190613c55565b920151906020818403910152613c55565b9181601f84011215613af75782359167ffffffffffffffff8311613af7576020808501948460051b010111613af757565b9181601f84011215613af75782359167ffffffffffffffff8311613af7576020808501948460081b010111613af757565b67ffffffffffffffff8111613cdd5760051b60200190565b81810292918115918404141715613f2057565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8115613f59570490565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b91908203918211613f2057565b519073ffffffffffffffffffffffffffffffffffffffff82168203613af757565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0938186528686013760008582860101520116010190565b92959390919473ffffffffffffffffffffffffffffffffffffffff600354169586156142cb578097600287101561429c5773ffffffffffffffffffffffffffffffffffffffff98614156957fffffffff0000000000000000000000000000000000000000000000000000000093896142725767ffffffffffffffff8216600052600b6020526040600020906040519161408d83613d44565b549163ffffffff8316815263ffffffff8360201c16602082015263ffffffff8360401c16604082015263ffffffff8360601c16606082015260c061ffff8460801c169182608082015260ff60a082019561ffff8160901c16875260a01c161515918291015261421e575b50505067ffffffffffffffff905b6040519b8c997f06b859ef000000000000000000000000000000000000000000000000000000008b521660048a0152166024880152604487015216606485015260c0608485015260c4840191613fb6565b928180600095869560a483015203915afa91821561421157819261417957505090565b9091503d8083833e61418b8183613d60565b810190602081830312610ae45780519067ffffffffffffffff821161070f570181601f82011215610ae4578051906141c282613ef5565b936141d06040519586613d60565b82855260208086019360051b8301019384116105db5750602001905b8282106141f95750505090565b6020809161420684613f95565b8152019101906141ec565b50604051903d90823e3d90fd5b92935067ffffffffffffffff928587161561425a575061271061424961ffff61425094511683613f0d565b0490613f88565b915b9038806140f7565b61426c92506142496127109183613f0d565b91614252565b67ffffffffffffffff91925061429690614290612a8336898b613da1565b9061496f565b91614105565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b50505050505050506040516142e1602082613d60565b60008152600036813790565b67ffffffffffffffff9092919261432b7fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1685614a7c565b16600052600b60205260406000206040519061434682613d44565b549163ffffffff83169384835263ffffffff8460201c169384602085015263ffffffff8160401c169182604086015263ffffffff8260601c169081606087015261ffff8360801c169586608082015260ff61ffff8560901c16948560a084015260a01c16159060c082159101526143f3577fffffffff00000000000000000000000000000000000000000000000000000000166143e857505093929190600190565b959493509160019150565b5050505092505050600090600090600090600090600090565b6040519061441982613d28565b60006080838281528260208201528260408201528260608201520152565b9060405161444481613d28565b60806001829460ff81546fffffffffffffffffffffffffffffffff8116865263ffffffff81861c16602087015260a01c161515604085015201546fffffffffffffffffffffffffffffffff81166060840152811c910152565b67ffffffffffffffff916144af61440c565b506144b861440c565b506144ec57166000526008602052604060002090613e216144e060026144e56144e086614437565b614b63565b9401614437565b16908160005260046020526145076144e06040600020614437565b916000526005602052613e216144e06040600020614437565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215613af7570180359067ffffffffffffffff8211613af757602001918136038313613af757565b3573ffffffffffffffffffffffffffffffffffffffff81168103613af75790565b3567ffffffffffffffff81168103613af75790565b9067ffffffffffffffff613e2192166000526008602052600560406000200190602081519101209060019160005201602052604060002054151590565b604051906145f182613d0c565b60606020838281520152565b80518210156146115760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90600182811c92168015614689575b602083101461465a57565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b91607f169161464f565b90604051918260008254926146a784614640565b808452936001811690811561471557506001146146ce575b506146cc92500383613d60565b565b90506000929192526020600020906000915b8183106146f95750509060206146cc92820101386146bf565b60209193508060019154838589010152019101909184926146e0565b602093506146cc9592507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0091501682840152151560051b820101386146bf565b67ffffffffffffffff166000526008602052613e216004604060002001614693565b91908110156146115760081b0190565b358015158103613af75790565b3561ffff81168103613af75790565b3563ffffffff81168103613af75790565b359063ffffffff82168203613af757565b359061ffff82168203613af757565b91908110156146115760051b0190565b35906fffffffffffffffffffffffffffffffff82168203613af757565b9190826060910312613af7576040516060810181811067ffffffffffffffff821117613cdd57604052604061485481839561483b81613cb4565b8552614849602082016147e4565b6020860152016147e4565b910152565b6fffffffffffffffffffffffffffffffff6148976040809361487a81613cb4565b151586528361488b602083016147e4565b166020870152016147e4565b16910152565b8181106148a8575050565b6000815560010161489d565b80518015614924576020036148e6578051602082810191830183900312613af757519060ff82116148e6575060ff1690565b61208c906040519182917f953576f7000000000000000000000000000000000000000000000000000000008352602060048401526024830190613c55565b50507f000000000000000000000000000000000000000000000000000000000000000090565b9060ff8091169116039060ff8211613f2057565b60ff16604d8111613f2057600a0a90565b907f00000000000000000000000000000000000000000000000000000000000000009060ff82169060ff811692828414614a7557828411614a4b57906149b49161494a565b91604d60ff8416118015614a12575b6149dc575050906149d6613e219261495e565b90613f0d565b9091507fa9cb113d0000000000000000000000000000000000000000000000000000000060005260045260245260445260646000fd5b50614a1c8361495e565b8015613f59577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0484116149c3565b614a549161494a565b91604d60ff8416116149dc57505090614a6f613e219261495e565b90613f4f565b5050505090565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115614b5e57614aaf816151ae565b7dffff00000000000000000000000000000000000000000000000000000000601082811c9085901c1616614b5e5761ffff8360e01c168015918215614b4d575b5050614af9575050565b7fffffffff0000000000000000000000000000000000000000000000000000000092507fdf63778f000000000000000000000000000000000000000000000000000000006000526004521660245260446000fd5b60e01c61ffff161090503880614aef565b505050565b614b6b61440c565b506fffffffffffffffffffffffffffffffff6060820151166fffffffffffffffffffffffffffffffff8083511691614bc86020850193614bc2614bb563ffffffff87511642613f88565b8560808901511690613f0d565b906151a1565b80821015614be157505b16825263ffffffff4216905290565b9050614bd2565b90816020910312613af757518015158103613af75790565b73ffffffffffffffffffffffffffffffffffffffff600154163303614c2157565b7f2b5c74de0000000000000000000000000000000000000000000000000000000060005260046000fd5b90805115614e8b5767ffffffffffffffff81516020830120921691826000526008602052614c80816005604060002001615805565b15614e475760005260096020526040600020815167ffffffffffffffff8111613cdd57614cad8254614640565b601f8111614e15575b506020601f8211600114614d4f5791614d29827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea9593614d3f95600091614d44575b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b9055604051918291602083526020830190613c55565b0390a2565b905084015138614cf8565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe082169083600052806000209160005b818110614dfd575092614d3f9492600192827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea989610614dc6575b5050811b019055611d3a565b8501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690553880614dba565b9192602060018192868a015181550194019201614d7f565b614e4190836000526020600020601f840160051c81019160208510610f3457601f0160051c019061489d565b38614cb6565b509061208c6040519283927f393b8ad20000000000000000000000000000000000000000000000000000000084526004840152604060248401526044830190613c55565b7f14c880ca0000000000000000000000000000000000000000000000000000000060005260046000fd5b60405160ff7f000000000000000000000000000000000000000000000000000000000000000016602082015260208152613e21604082613d60565b815191929115615072576fffffffffffffffffffffffffffffffff6040840151166fffffffffffffffffffffffffffffffff6020850151161061500f576146cc91925b805182547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff0000000000000000000000000000000000000000161782556020810151825460409290920151608090811b7fffffffffffffffffffffffffffffffff00000000000000000000000000000000166fffffffffffffffffffffffffffffffff9290921691821760018501557fffffffffffffffffffffffff0000000000000000000000000000000000000000909216174290911b73ffffffff0000000000000000000000000000000016179055565b606483615070604051917f8020d12400000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565bfd5b6fffffffffffffffffffffffffffffffff60408401511615801590615101575b6150a0576146cc9192614f33565b606483615070604051917fd68af9cc00000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b506fffffffffffffffffffffffffffffffff6020840151161515615092565b906127109167ffffffffffffffff61513a60208301614592565b166000908152600b60205260409020917fffffffff00000000000000000000000000000000000000000000000000000000161561518b57606061ffff615187935460901c16910135613f0d565b0490565b606061ffff615187935460801c16910135613f0d565b91908201809211613f2057565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115615285577dffff0000000000000000000000000000000000000000000000000000000081161561527c5760ff60015b169060f01c80615246575b506001036152195750565b7fc512f96c0000000000000000000000000000000000000000000000000000000060005260045260246000fd5b60005b60108110615257575061520e565b6001811b821661526a575b600101615249565b9160018101809111613f205791615262565b60ff6000615203565b5050565b9167ffffffffffffffff7f50f6fbee3ceedce6b7fd7eaef18244487867e6718aec7208187efb6b7908c14c9216928360005260086020526152d28183600260406000200161585a565b6040805173ffffffffffffffffffffffffffffffffffffffff909216825260208201929092529081908101614d3f565b91909167ffffffffffffffff83169283600052600560205260ff60406000205460a01c16156153675750907fc6735cd4fa2bbe7b203b1682936e6ee61bc1702464bbbd12abb6630229d9a5f9918360005260056020526152d28183604060002061585a565b906146cc9350615289565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe182360301811215613af757016020813591019167ffffffffffffffff8211613af7578136038313613af757565b9167ffffffffffffffff7fff0133389f9bb82d5b9385826160eaf2328039f6fa950eeb8cf0836da81789449216928360005260086020526152d28183604060002061585a565b91909167ffffffffffffffff83169283600052600460205260ff60406000205460a01c161561546d5750907f28d6c52e2b0b7587b0d195539fbe6af984b28791aca4d2cc0844244e38bce29e918360005260046020526152d28183604060002061585a565b906146cc93506153c2565b906040519182815491828252602082019060005260206000209260005b8181106154aa5750506146cc92500383613d60565b8454835260019485019487945060209093019201615495565b80548210156146115760005260206000200190600090565b600081815260076020526040902054801561566a577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613f2057600654907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613f20578181036155fb575b50505060065480156155cc577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff016155898160066154c3565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b19169055600655600052600760205260006040812055600190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b61565261560c61561d9360066154c3565b90549060031b1c92839260066154c3565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b90556000526007602052604060002055388080615550565b5050600090565b906001820191816000528260205260406000205480151560001461579c577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613f20578254907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613f2057818103615765575b505050805480156155cc577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff019061572682826154c3565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b191690555560005260205260006040812055600190565b61578561577561561d93866154c3565b90549060031b1c928392866154c3565b9055600052836020526040600020553880806156ee565b50505050600090565b806000526007602052604060002054156000146157ff5760065468010000000000000000811015613cdd576157e661561d82600185940160065560066154c3565b9055600654906000526007602052604060002055600190565b50600090565b600082815260018201602052604090205461566a5780549068010000000000000000821015613cdd578261584361561d8460018096018555846154c3565b905580549260005201602052604060002055600190565b9182549060ff8260a01c16158015615b0e575b615b08576fffffffffffffffffffffffffffffffff821691600185019081546158b263ffffffff6fffffffffffffffffffffffffffffffff83169360801c1642613f88565b9081615a6a575b5050848110615a1e57508383106159135750506158e86fffffffffffffffffffffffffffffffff928392613f88565b16167fffffffffffffffffffffffffffffffff00000000000000000000000000000000825416179055565b9190915460801c9283156159b2578161592b91613f88565b927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810190808211613f205761597961597e9273ffffffffffffffffffffffffffffffffffffffff966151a1565b613f4f565b7fd0c8d23a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b73ffffffffffffffffffffffffffffffffffffffff83837fd0c8d23a000000000000000000000000000000000000000000000000000000006000527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6004526024521660445260646000fd5b828573ffffffffffffffffffffffffffffffffffffffff927f1a76572a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b828692939611615ade57615a8592614bc29160801c90613f0d565b80841015615ad95750825b85547fffffffffffffffffffffffff00000000ffffffffffffffffffffffffffffffff164260801b73ffffffff00000000000000000000000000000000161786559238806158b9565b615a90565b7f9725942a0000000000000000000000000000000000000000000000000000000060005260046000fd5b50505050565b50821561586d56fea164736f6c634300081a000a' as const +// generate:end diff --git a/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/burn-mint-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/burn-mint-token-pool.ts new file mode 100644 index 00000000..a00f5f88 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/burn-mint-token-pool.ts @@ -0,0 +1,4 @@ +export default // generate: +// `'${require('fs').readFileSync(require.resolve('@chainlink/contracts-ccip/bytecode/v2_0_0/burn_mint_token_pool.bin'), 'utf8').trim()}' as const` +'0x60e080604052346101f65760a081615db2803803809161001f8285610247565b8339810103126101f65780516001600160a01b038116908190036101f65761004960208301610280565b6100556040840161028e565b9161006e60806100676060870161028e565b950161028e565b93331561023657600180546001600160a01b0319163317905581158015610225575b8015610214575b610203578160805260c052308103610170575b5060a052600380546001600160a01b039283166001600160a01b03199182161790915560028054939092169216919091179055604051615b0f90816102a3823960805181818161023e01528181610491015281816122660152818161243e01528181612aa101528181612c9c0152818161318e0152818161373b0152613795015260a051818181613601015281816149140152818161495e0152614ea8015260c0518181816102d9015281816113eb0152818161230001528181612b3c01526132290152f35b60206004916040519283809263313ce56760e01b82525afa600091816101c2575b50156100aa5760ff1660ff82168181036101ab57506100aa565b6332ad3e0760e11b60005260045260245260446000fd5b9091506020813d6020116101fb575b816101de60209383610247565b810103126101f6576101ef90610280565b9038610191565b600080fd5b3d91506101d1565b630a64406560e11b60005260046000fd5b506001600160a01b03811615610097565b506001600160a01b03851615610090565b639b15e16f60e01b60005260046000fd5b601f909101601f19168101906001600160401b0382119082101761026a57604052565b634e487b7160e01b600052604160045260246000fd5b519060ff821682036101f657565b51906001600160a01b03821682036101f65756fe608080604052600436101561001357600080fd5b600090813560e01c90816301ffc9a7146139b65750806306b859ef146138d1578063181f5a77146138705780631826b1e7146137b957806321df0da714613768578063240028e8146137045780632422ac451461362557806324f65ee7146135e75780632cab0fb6146130f357806337a3210d146130bf57806339077537146129f65780634c5ef0ed146129af57806362ddd3c4146129285780637437ff9f146128da57806379ba5097146128135780638926f54f146127cd5780638da5cb5b146127995780639a4575b9146121ed578063a42a7b8b14612086578063acfecf9114611f8e578063ae39a25714611e03578063b6cfa3b714611d48578063b794658014611d10578063bfeffd3f14611c64578063c4bffe2b14611b39578063c7230a6014611893578063dc04fa1f1461140f578063dc0bd971146113be578063dcbd41bc146111ba578063e8a1da1714610ade578063ea6396db146109a0578063ec6ae7a71461095d578063f2fde38b1461088e5763fbc801a71461019757600080fd5b346105d15760606003193601126105d1576004359067ffffffffffffffff82116105d1578160040160a060031984360301126105df576101d5613ae8565b9060443567ffffffffffffffff811161070557906101fa610217923690600401613c13565b92906102046145d0565b5061020f858461510c565b933691613d8d565b9260848601936102268561455d565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361084457602487019677ffffffffffffffff0000000000000000000000000000000061028c8961457e565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156107b7578891610815575b506107ed5767ffffffffffffffff6103208961457e565b16610338816000526007602052604060002054151590565b156107c257602073ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156107b7578890610766575b73ffffffffffffffffffffffffffffffffffffffff915016330361073a576064810135936103c78686613f74565b7fffffffff00000000000000000000000000000000000000000000000000000000851694851561071857610423907fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1690614a68565b61043f816104308a61455d565b6104398d61457e565b906153f4565b73ffffffffffffffffffffffffffffffffffffffff6003541693846105e3575b5050505050509061046f91613f74565b916104798461457e565b5073ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b156105df578180916024604051809481937f42966c680000000000000000000000000000000000000000000000000000000083528960048401525af180156105d4576105bc575b6105b28461058161057c88877ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff61054261053c8561457e565b9361455d565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252336020830152810188905292169180606081015b0390a261457e565b614741565b9061058a614ea1565b6040519261059784613cf8565b83526020830152604051928392604084526040840190613e55565b9060208301520390f35b6105c7828092613d4c565b6105d157806104fa565b80fd5b6040513d84823e3d90fd5b5080fd5b843b15610714578994928b9694928692604051988997889687957fa8027c0f0000000000000000000000000000000000000000000000000000000087526004870160809052806106329161535e565b6084880160a0905261012488019061064992613fa2565b9261065390613bfe565b67ffffffffffffffff1660a487015260440161066e90613baf565b73ffffffffffffffffffffffffffffffffffffffff1660c48601528d8c60e487015261069990613baf565b73ffffffffffffffffffffffffffffffffffffffff1661010486015260248501528381036003190160448501526106cf91613c41565b90606483015203925af18015610709579085916106f0575b8080808061045f565b816106fa91613d4c565b6107055783386106e7565b8380fd5b6040513d87823e3d90fd5b8980fd5b50610735816107268a61455d565b61072f8d61457e565b906153ae565b61043f565b6024877f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b506020813d6020116107af575b8161078060209383613d4c565b810103126107ab576107a673ffffffffffffffffffffffffffffffffffffffff91613f81565b610399565b8780fd5b3d9150610773565b6040513d8a823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008852600452602487fd5b6004877f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b610837915060203d60201161083d575b61082f8183613d4c565b810190614bd4565b38610309565b503d610825565b60248673ffffffffffffffffffffffffffffffffffffffff6108658861455d565b7f961c9a4f00000000000000000000000000000000000000000000000000000000835216600452fd5b50346105d15760206003193601126105d15773ffffffffffffffffffffffffffffffffffffffff6108bd613b46565b6108c5614bec565b1633811461093557807fffffffffffffffffffffffff000000000000000000000000000000000000000083541617825573ffffffffffffffffffffffffffffffffffffffff600154167fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12788380a380f35b6004827fdad89dca000000000000000000000000000000000000000000000000000000008152fd5b50346105d157806003193601126105d15760207fffffffff0000000000000000000000000000000000000000000000000000000060025460401b16604051908152f35b50346105d15760806003193601126105d1576109ba613b46565b506109c3613bd0565b6109cb613b17565b5060643567ffffffffffffffff8111610ada579167ffffffffffffffff6040926109fb60e0953690600401613c13565b50508260c08551610a0b81613d30565b82815282602082015282878201528260608201528260808201528260a08201520152168152600b6020522060405190610a4382613d30565b5461ffff818163ffffffff82169485815263ffffffff60208201818560201c1681528160408401818760401c168152816060860193818960601c16855260ff60c060808901988a8c60801c168a528a60a082019c60901c168c52019b60a01c1615158b526040519b8c52511660208b0152511660408901525116606087015251166080850152511660a083015251151560c0820152f35b8280fd5b50346105d15760406003193601126105d15760043567ffffffffffffffff81116105df57610b10903690600401613e7f565b9060243567ffffffffffffffff81116107055790610b3384923690600401613e7f565b939091610b3e614bec565b83905b828210610ffb5750505081927ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee182360301935b81811015610ff7578060051b83013585811215610ff357830161012081360312610ff35760405194610ba586613d14565b610bae82613bfe565b8652602082013567ffffffffffffffff81116105df5782019436601f870112156105df57853595610bde87613ee1565b96610bec6040519889613d4c565b80885260208089019160051b83010190368211610ff35760208301905b828210610fc0575050505060208701958652604083013567ffffffffffffffff8111610ada57610c3c9036908501613df2565b9160408801928352610c66610c5436606087016147ed565b9460608a0195865260c03691016147ed565b956080890196875283515115610f9857610c8a67ffffffffffffffff8a5116615791565b15610f615767ffffffffffffffff8951168252600860205260408220610cb1865182614edc565b610cbf885160028301614edc565b6004855191019080519067ffffffffffffffff8211610f3457610ce2835461462c565b601f8111610ef9575b50602090601f8311600114610e5a57610d399291869183610e4f575b50507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b90555b815b88518051821015610d735790610d6d600192610d668367ffffffffffffffff8f5116926145e9565b5190614c37565b01610d3e565b5050977f8d340f17e19058004c20453540862a9c62778504476f6756755cb33bcd6c38c2939199975095610e4167ffffffffffffffff6001979694985116925193519151610e0d610dd860405196879687526101006020880152610100870190613c41565b9360408601906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b60a08401906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b0390a1019392909193610b74565b015190508e80610d07565b83865281862091907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08416875b818110610ee15750908460019594939210610eaa575b505050811b019055610d3c565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690558d8080610e9d565b92936020600181928786015181550195019301610e87565b610f249084875260208720601f850160051c81019160208610610f2a575b601f0160051c0190614889565b8d610ceb565b9091508190610f17565b6024857f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b60248267ffffffffffffffff8b51167f1d5ad3c5000000000000000000000000000000000000000000000000000000008252600452fd5b6004827f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b813567ffffffffffffffff8111610fef57602091610fe48392833691890101613df2565b815201910190610c09565b8680fd5b8480fd5b8380f35b9267ffffffffffffffff61101d6110188486889a9699979a6147c0565b61457e565b1691611028836154c7565b1561118e57828452600860205261104460056040862001615464565b94845b865181101561107d5760019085875260086020526110766005604089200161106f838b6145e9565b519061565d565b5001611047565b50939692909450949094808752600860205260056040882088815588600182015588600282015588600382015588600482016110b9815461462c565b8061114d575b505050018054908881558161112f575b5050907f5204aec90a3c794d8e90fded8b46ae9c7c552803e7e832e0c1d358396d8599166020836001948a52600482528985604082208281550155808a52600582528985604082208281550155604051908152a101909194939294610b41565b885260208820908101905b818110156110cf5788815560010161113a565b601f81116001146111635750555b888a806110bf565b8183526020832061117e91601f01861c810190600101614889565b808252816020812091555561115b565b602484847f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b50346105d15760206003193601126105d15760043567ffffffffffffffff81116105df576111ec903690600401613eb0565b73ffffffffffffffffffffffffffffffffffffffff600a54163314158061139c575b61137057825b81811061121f578380f35b61122a818385614763565b67ffffffffffffffff61123c8261457e565b1690611255826000526007602052604060002054151590565b1561134457907f41f7c8f7cfdad9350aa495e6c54cbbf750a07ab38a9098aed1256e30dd1682bb60e0836113046112de602060019897018b61129682614773565b1561130b5787905260046020526112bd60408d206112b736604088016147ed565b90614edc565b868c5260056020526112d960408d206112b73660a088016147ed565b614773565b9160405192151583526112f76020840160408301614845565b60a0608084019101614845565ba201611214565b60026040828a6112d99452600860205261132d8282206112b736858c016147ed565b8a8152600860205220016112b73660a088016147ed565b602486837f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b6024837f8e4a23d600000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff6001541633141561120e565b50346105d157806003193601126105d157602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105d15760406003193601126105d15760043567ffffffffffffffff81116105df57611441903690600401613eb0565b60243567ffffffffffffffff811161070557611461903690600401613e7f565b91909261146c614bec565b845b8281106114d857505050825b818110611485578380f35b8067ffffffffffffffff61149f61101860019486886147c0565b16808652600b6020528560408120557f5479bbc0288b7eaeaf2ace0943b88016cc648964fcd42919a86fd93b15fdbee88680a20161147a565b67ffffffffffffffff6114ef611018838686614763565b16611507816000526007602052604060002054151590565b1561186857611517828585614763565b602081019060e081019061152a82614773565b1561183c5760a0810161271061ffff61154283614780565b16101561182d5760c082019161271061ffff61155d85614780565b1610156117f55763ffffffff6115728661478f565b16156117c957858c52600b60205260408c2061158d8661478f565b63ffffffff169080549060408401916115a58361478f565b60201b67ffffffff00000000169360608601946115c18661478f565b60401b6bffffffff00000000000000001696608001966115e08861478f565b60601b6fffffffff00000000000000000000000016916115ff8a614780565b60801b71ffff0000000000000000000000000000000016936116208c614780565b60901b73ffff00000000000000000000000000000000000016957fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016177fffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffff16177fffffffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffff16177fffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffffffff1617171781556116d387614773565b81547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff00000000000000000000000000000000000000001617905560405196611724906147a0565b63ffffffff168752611735906147a0565b63ffffffff166020870152611749906147a0565b63ffffffff16604086015261175d906147a0565b63ffffffff166060850152611771906147b1565b61ffff166080840152611783906147b1565b61ffff1660a083015261179590613ca0565b151560c082015260e07ffae1e296719dac5269c3886fb5002bb29bf17ae403060c6eb063a55abaaa104191a260010161146e565b60248c877f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b60248c61ffff61180486614780565b7f95f3517a00000000000000000000000000000000000000000000000000000000835216600452fd5b8a61ffff611804602493614780565b60248a857f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b7f1e670e4b000000000000000000000000000000000000000000000000000000008752600452602486fd5b50346105d15760406003193601126105d15760043567ffffffffffffffff81116105df576118c5903690600401613e7f565b906118ce613b8c565b9173ffffffffffffffffffffffffffffffffffffffff6001541633141580611b17575b611aeb5773ffffffffffffffffffffffffffffffffffffffff8316908115611ac357845b818110611920578580f35b73ffffffffffffffffffffffffffffffffffffffff6119486119438385886147c0565b61455d565b166040517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152602081602481855afa9081156107b7578891611a90575b508061199d575b5050600101611915565b6040517fa9059cbb00000000000000000000000000000000000000000000000000000000602080830191825273ffffffffffffffffffffffffffffffffffffffff8a16602484015260448084018590528352918a91906119fe606482613d4c565b519082865af115611a855787513d611a7c5750813b155b611a505790847f508d7d183612c18fc339b42618912b9fa3239f631dd7ec0671f950200a0fa66e602060019594604051908152a39038611993565b602488837f5274afe7000000000000000000000000000000000000000000000000000000008252600452fd5b60011415611a15565b6040513d89823e3d90fd5b905060203d8111611abc575b611aa68183613d4c565b602082600092810103126105d15750513861198c565b503d611a9c565b6004857f8579befe000000000000000000000000000000000000000000000000000000008152fd5b6024847fcb1afbd700000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff600c54163314156118f1565b50346105d157806003193601126105d157604051906006548083528260208101600684526020842092845b818110611c4b575050611b7992500383613d4c565b8151611b9d611b8782613ee1565b91611b956040519384613d4c565b808352613ee1565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0602083019301368437805b8451811015611bfc578067ffffffffffffffff611be9600193886145e9565b5116611bf582866145e9565b5201611bca565b50925090604051928392602084019060208552518091526040840192915b818110611c28575050500390f35b825167ffffffffffffffff16845285945060209384019390920191600101611c1a565b8454835260019485019487945060209093019201611b64565b50346105d15760206003193601126105d15760043573ffffffffffffffffffffffffffffffffffffffff81168091036105df57611c9f614bec565b7fffffffffffffffffffffffff00000000000000000000000000000000000000006003547fbaff46844acf36d6ee996f489a1a288709c4542bd33cd557770afd267d5812096040805173ffffffffffffffffffffffffffffffffffffffff84168152856020820152a1161760035580f35b50346105d15760206003193601126105d157611d44611d3061057c613be7565b604051918291602083526020830190613c41565b0390f35b50346105d15760206003193601126105d1577f307cf716eade81675bea3ccb6917b0f91baa2160056765d9a83d76f819caf06a6020611d85613ab4565b611d8d614bec565b6002547fffffffffffffffff00000000ffffffffffffffffffffffffffffffffffffffff77ffffffff0000000000000000000000000000000000000000808460401c16169116176002557fffffffff0000000000000000000000000000000000000000000000000000000060405191168152a180f35b50346105d15760606003193601126105d157611e1d613b46565b90611e26613b8c565b6044359273ffffffffffffffffffffffffffffffffffffffff841680850361070557611e50614bec565b73ffffffffffffffffffffffffffffffffffffffff82168015611f665794611f60917f3f1036e85d016a93254a0b1415844f79b85424959d90ae5ad51ce8f4533fe70195967fffffffffffffffffffffffff0000000000000000000000000000000000000000600254161760025573ffffffffffffffffffffffffffffffffffffffff85167fffffffffffffffffffffffff0000000000000000000000000000000000000000600a541617600a557fffffffffffffffffffffffff0000000000000000000000000000000000000000600c541617600c556040519384938491604091949373ffffffffffffffffffffffffffffffffffffffff809281606087019816865216602085015216910152565b0390a180f35b6004857f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b50346105d15767ffffffffffffffff611fa636613e10565b929091611fb1614bec565b1691611fca836000526007602052604060002054151590565b1561118e578284526008602052611ff960056040862001611fec368486613d8d565b602081519101209061565d565b1561203e57907f52d00ee4d9bd51b40168f2afc5848837288ce258784ad914278791464b3f4d7691612038604051928392602084526020840191613fa2565b0390a280f35b82612082836040519384937f74f23c7c0000000000000000000000000000000000000000000000000000000085526004850152604060248501526044840191613fa2565b0390fd5b50346105d15760206003193601126105d15767ffffffffffffffff6120a9613be7565b16815260086020526120c060056040832001615464565b80517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe06121056120ef83613ee1565b926120fd6040519485613d4c565b808452613ee1565b01835b8181106121dc575050825b82518110156121595780612129600192856145e9565b518552600960205261213d6040862061467f565b61214782856145e9565b5261215281846145e9565b5001612113565b81846040519182916020830160208452825180915260408401602060408360051b870101940192905b82821061219157505050500390f35b919360206121cc827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc060019597998495030186528851613c41565b9601920192018594939192612182565b806060602080938601015201612108565b50346105d15760206003193601126105d15760043567ffffffffffffffff81116105df57806004019060a06003198236030112610ada5761222c6145d0565b5060405160209361223d8583613d4c565b808252608483019161224e8361455d565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361277857602484019477ffffffffffffffff000000000000000000000000000000006122b48761457e565b60801b16604051907f2cbc26bb0000000000000000000000000000000000000000000000000000000082526004820152878160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156126fd57849161275b575b506127335767ffffffffffffffff6123478761457e565b1661235f816000526007602052604060002054151590565b15612708578773ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156126fd5784906126b5575b73ffffffffffffffffffffffffffffffffffffffff9150163303612689576064850135946123f9866123f08761455d565b61072f8a61457e565b73ffffffffffffffffffffffffffffffffffffffff60035416918261256c575b505050506124268461457e565b5073ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b156105df578180916024604051809481937f42966c680000000000000000000000000000000000000000000000000000000083528960048401525af180156105d457612557575b8561252761057c87877ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff896105746124f06124ea8761457e565b9261455d565b6040805173ffffffffffffffffffffffffffffffffffffffff90921682523360208301528101959095529116929081906060820190565b90612530614ea1565b6040519261253d84613cf8565b835281830152611d44604051928284938452830190613e55565b612562828092613d4c565b6105d157806124a7565b823b15610ff357918791858094604051968795869485937fa8027c0f0000000000000000000000000000000000000000000000000000000085526004850160809052806125b89161535e565b6084860160a090526101248601906125cf92613fa2565b916125d990613bfe565b67ffffffffffffffff1660a48501526044016125f490613baf565b73ffffffffffffffffffffffffffffffffffffffff1660c48401528b60e484015261261e8b613baf565b73ffffffffffffffffffffffffffffffffffffffff1661010484015283602484015282810360031901604484015261265591613c41565b8a606483015203925af180156105d457908291612674575b8080612419565b8161267e91613d4c565b6105d157803861266d565b6024837f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b508781813d83116126f6575b6126cb8183613d4c565b81010312610705576126f173ffffffffffffffffffffffffffffffffffffffff91613f81565b6123bf565b503d6126c1565b6040513d86823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008452600452602483fd5b6004837f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b6127729150883d8a1161083d5761082f8183613d4c565b38612330565b5073ffffffffffffffffffffffffffffffffffffffff61086560249361455d565b50346105d157806003193601126105d157602073ffffffffffffffffffffffffffffffffffffffff60015416604051908152f35b50346105d15760206003193601126105d157602061280967ffffffffffffffff6127f5613be7565b166000526007602052604060002054151590565b6040519015158152f35b50346105d157806003193601126105d157805473ffffffffffffffffffffffffffffffffffffffff811633036128b2577fffffffffffffffffffffffff000000000000000000000000000000000000000060015491338284161760015516825573ffffffffffffffffffffffffffffffffffffffff3391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08380a380f35b6004827f02b543c6000000000000000000000000000000000000000000000000000000008152fd5b50346105d157806003193601126105d157600254600a54600c546040805173ffffffffffffffffffffffffffffffffffffffff94851681529284166020840152921691810191909152606090f35b50346105d15761293736613e10565b61294393929193614bec565b67ffffffffffffffff8216612965816000526007602052604060002054151590565b156129845750612981929361297b913691613d8d565b90614c37565b80f35b7f1e670e4b000000000000000000000000000000000000000000000000000000008452600452602483fd5b50346105d15760406003193601126105d1576129c9613be7565b906024359067ffffffffffffffff82116105d1576020612809846129f03660048701613df2565b90614593565b50346105d15760206003193601126105d1576004359067ffffffffffffffff82116105d157816004019061010060031984360301126105d15780604051612a3c81613cad565b5280604051612a4a81613cad565b52606483013560c4840193612a7a612a74612a6f612a68888861450c565b3691613d8d565b6148a0565b8361495b565b936084820195612a898761455d565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361309e57602483019377ffffffffffffffff00000000000000000000000000000000612aef8661457e565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115611a8557879161307f575b506130575767ffffffffffffffff612b838661457e565b16612b9b816000526007602052604060002054151590565b1561302c57602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa908115611a8557879161300d575b5015612fe157612c128561457e565b92612c2860a48601946129f0612a68878561450c565b15612f9a57612c4988612c3a8b61455d565b612c438961457e565b90615275565b73ffffffffffffffffffffffffffffffffffffffff600354169283612dcc575b505050505060440191612c7b8361455d565b612c848361457e565b5073ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001690813b15610ada576040517f40c10f1900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff919091166004820152602481018690529082908290604490829084905af180156105d457612db7575b5050608067ffffffffffffffff60209573ffffffffffffffffffffffffffffffffffffffff612d83612d7d61053c7ffc5e3a5bddc11d92c2dc20fae6f7d5eb989f056be35239f7de7e86150609abc09761457e565b9661455d565b816040519716875233898801521660408601528560608601521692a260405190612dac82613cad565b815260405190518152f35b612dc2828092613d4c565b6105d15780612d28565b833b156107ab57878795938195938c93604051988997889687957f6371157400000000000000000000000000000000000000000000000000000000875260048701606090528d612e1c878061535e565b60648a0161010090526101648a0190612e3492613fa2565b94612e3e90613bfe565b67ffffffffffffffff166084890152604401612e5990613baf565b73ffffffffffffffffffffffffffffffffffffffff1660a488015260c4870152612e8290613baf565b73ffffffffffffffffffffffffffffffffffffffff1660e4860152612ea7908461535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c86840301610104870152612edc9291613fa2565b90612ee7908361535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c85840301610124860152612f1c9291613fa2565b9060e48a01612f2a9161535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c84840301610144850152612f5f9291613fa2565b8b602483015282604483015203925af180156126fd57908491612f85575b808080612c69565b81612f8f91613d4c565b610ada578238612f7d565b83612fa49161450c565b6120826040519283927f24eb47e5000000000000000000000000000000000000000000000000000000008452602060048501526024840191613fa2565b6024867f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b613026915060203d60201161083d5761082f8183613d4c565b38612c03565b7fa9902c7e000000000000000000000000000000000000000000000000000000008752600452602486fd5b6004867f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b613098915060203d60201161083d5761082f8183613d4c565b38612b6c565b60248573ffffffffffffffffffffffffffffffffffffffff6108658a61455d565b50346105d157806003193601126105d157602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b50346105d15760406003193601126105d1576004359067ffffffffffffffff82116105d157816004019061010060031984360301126105d157613134613ae8565b918160405161314281613cad565b5260648401359360c4810193613167613161612a6f612a68888761450c565b8761495b565b9460848301966131768861455d565b73ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116036135c657602484019477ffffffffffffffff000000000000000000000000000000006131dc8761457e565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156107b75788916135a7575b506107ed5767ffffffffffffffff6132708761457e565b16613288816000526007602052604060002054151590565b156107c257602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa9081156107b7578891613588575b501561073a576132ff8661457e565b9361331560a48701956129f0612a68888661450c565b1561357e577fffffffff00000000000000000000000000000000000000000000000000000000169081156135635761335f896133508c61455d565b6133598a61457e565b906152ee565b73ffffffffffffffffffffffffffffffffffffffff600354169384613392575b50505050505060440191612c7b8361455d565b843b1561355f57868995938c959387938b6040519a8b998a9889977f6371157400000000000000000000000000000000000000000000000000000000895260048901606090526133e2878061535e565b60648b0161010090526101648b01906133fa92613fa2565b9461340490613bfe565b67ffffffffffffffff1660848a015260440161341f90613baf565b73ffffffffffffffffffffffffffffffffffffffff1660a489015260c488015261344890613baf565b73ffffffffffffffffffffffffffffffffffffffff1660e487015261346d908461535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c878403016101048801526134a29291613fa2565b906134ad908361535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c868403016101248701526134e29291613fa2565b9060e48b016134f09161535e565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c858403016101448601526135259291613fa2565b908c6024840152604483015203925af180156126fd5761354a575b808080808061337f565b926135588160449395613d4c565b9290613540565b8880fd5b613579896135708c61455d565b612c438a61457e565b61335f565b612fa4858361450c565b6135a1915060203d60201161083d5761082f8183613d4c565b386132f0565b6135c0915060203d60201161083d5761082f8183613d4c565b38613259565b60248673ffffffffffffffffffffffffffffffffffffffff6108658b61455d565b50346105d157806003193601126105d157602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105d15760406003193601126105d15761363f613be7565b6024359182151583036105d15761014061370261365c8585614489565b6136b260409392935180946fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565b60a08301906fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565bf35b50346105d15760206003193601126105d157602090613721613b46565b905073ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116146040519015158152f35b50346105d157806003193601126105d157602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105d15760c06003193601126105d1576137d3613b46565b506137dc613bd0565b6137e4613b69565b50608435917fffffffff00000000000000000000000000000000000000000000000000000000831683036105d15760a4359067ffffffffffffffff82116105d15760a063ffffffff8061ffff61384988886138423660048b01613c13565b50506142d9565b95926040979194975197885216602087015216604085015216606083015215156080820152f35b50346105d157806003193601126105d15750611d44604051613893604082613d4c565b601781527f4275726e4d696e74546f6b656e506f6f6c20322e302e300000000000000000006020820152604051918291602083526020830190613c41565b50346105d15760c06003193601126105d1576138eb613b46565b6138f3613bd0565b906064357fffffffff00000000000000000000000000000000000000000000000000000000811681036107055760843567ffffffffffffffff8111610ff357613940903690600401613c13565b9160a435936002851015610fef5761395b9560443591613fe1565b90604051918291602083016020845282518091526020604085019301915b818110613987575050500390f35b825173ffffffffffffffffffffffffffffffffffffffff16845285945060209384019390920191600101613979565b9050346105df5760206003193601126105df576020907fffffffff000000000000000000000000000000000000000000000000000000006139f5613ab4565b167faff2afbf000000000000000000000000000000000000000000000000000000008114908115613a8a575b8115613a60575b8115613a36575b5015158152f35b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501483613a2f565b7f0e64dd290000000000000000000000000000000000000000000000000000000081149150613a28565b7f940a15420000000000000000000000000000000000000000000000000000000081149150613a21565b600435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613ae357565b600080fd5b602435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613ae357565b604435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613ae357565b6004359073ffffffffffffffffffffffffffffffffffffffff82168203613ae357565b6064359073ffffffffffffffffffffffffffffffffffffffff82168203613ae357565b6024359073ffffffffffffffffffffffffffffffffffffffff82168203613ae357565b359073ffffffffffffffffffffffffffffffffffffffff82168203613ae357565b6024359067ffffffffffffffff82168203613ae357565b6004359067ffffffffffffffff82168203613ae357565b359067ffffffffffffffff82168203613ae357565b9181601f84011215613ae35782359167ffffffffffffffff8311613ae35760208381860195010111613ae357565b919082519283825260005b848110613c8b5750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8460006020809697860101520116010190565b80602080928401015182828601015201613c4c565b35908115158203613ae357565b6020810190811067ffffffffffffffff821117613cc957604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040810190811067ffffffffffffffff821117613cc957604052565b60a0810190811067ffffffffffffffff821117613cc957604052565b60e0810190811067ffffffffffffffff821117613cc957604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117613cc957604052565b92919267ffffffffffffffff8211613cc95760405191613dd5601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200184613d4c565b829481845281830111613ae3578281602093846000960137010152565b9080601f83011215613ae357816020613e0d93359101613d8d565b90565b906040600319830112613ae35760043567ffffffffffffffff81168103613ae357916024359067ffffffffffffffff8211613ae357613e5191600401613c13565b9091565b613e0d916020613e6e8351604084526040840190613c41565b920151906020818403910152613c41565b9181601f84011215613ae35782359167ffffffffffffffff8311613ae3576020808501948460051b010111613ae357565b9181601f84011215613ae35782359167ffffffffffffffff8311613ae3576020808501948460081b010111613ae357565b67ffffffffffffffff8111613cc95760051b60200190565b81810292918115918404141715613f0c57565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8115613f45570490565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b91908203918211613f0c57565b519073ffffffffffffffffffffffffffffffffffffffff82168203613ae357565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0938186528686013760008582860101520116010190565b92959390919473ffffffffffffffffffffffffffffffffffffffff600354169586156142b757809760028710156142885773ffffffffffffffffffffffffffffffffffffffff98614142957fffffffff00000000000000000000000000000000000000000000000000000000938961425e5767ffffffffffffffff8216600052600b6020526040600020906040519161407983613d30565b549163ffffffff8316815263ffffffff8360201c16602082015263ffffffff8360401c16604082015263ffffffff8360601c16606082015260c061ffff8460801c169182608082015260ff60a082019561ffff8160901c16875260a01c161515918291015261420a575b50505067ffffffffffffffff905b6040519b8c997f06b859ef000000000000000000000000000000000000000000000000000000008b521660048a0152166024880152604487015216606485015260c0608485015260c4840191613fa2565b928180600095869560a483015203915afa9182156141fd57819261416557505090565b9091503d8083833e6141778183613d4c565b810190602081830312610ada5780519067ffffffffffffffff8211610705570181601f82011215610ada578051906141ae82613ee1565b936141bc6040519586613d4c565b82855260208086019360051b8301019384116105d15750602001905b8282106141e55750505090565b602080916141f284613f81565b8152019101906141d8565b50604051903d90823e3d90fd5b92935067ffffffffffffffff9285871615614246575061271061423561ffff61423c94511683613ef9565b0490613f74565b915b9038806140e3565b61425892506142356127109183613ef9565b9161423e565b67ffffffffffffffff9192506142829061427c612a6f36898b613d8d565b9061495b565b916140f1565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b50505050505050506040516142cd602082613d4c565b60008152600036813790565b67ffffffffffffffff909291926143177fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1685614a68565b16600052600b60205260406000206040519061433282613d30565b549163ffffffff83169384835263ffffffff8460201c169384602085015263ffffffff8160401c169182604086015263ffffffff8260601c169081606087015261ffff8360801c169586608082015260ff61ffff8560901c16948560a084015260a01c16159060c082159101526143df577fffffffff00000000000000000000000000000000000000000000000000000000166143d457505093929190600190565b959493509160019150565b5050505092505050600090600090600090600090600090565b6040519061440582613d14565b60006080838281528260208201528260408201528260608201520152565b9060405161443081613d14565b60806001829460ff81546fffffffffffffffffffffffffffffffff8116865263ffffffff81861c16602087015260a01c161515604085015201546fffffffffffffffffffffffffffffffff81166060840152811c910152565b67ffffffffffffffff9161449b6143f8565b506144a46143f8565b506144d857166000526008602052604060002090613e0d6144cc60026144d16144cc86614423565b614b4f565b9401614423565b16908160005260046020526144f36144cc6040600020614423565b916000526005602052613e0d6144cc6040600020614423565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215613ae3570180359067ffffffffffffffff8211613ae357602001918136038313613ae357565b3573ffffffffffffffffffffffffffffffffffffffff81168103613ae35790565b3567ffffffffffffffff81168103613ae35790565b9067ffffffffffffffff613e0d92166000526008602052600560406000200190602081519101209060019160005201602052604060002054151590565b604051906145dd82613cf8565b60606020838281520152565b80518210156145fd5760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90600182811c92168015614675575b602083101461464657565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b91607f169161463b565b90604051918260008254926146938461462c565b808452936001811690811561470157506001146146ba575b506146b892500383613d4c565b565b90506000929192526020600020906000915b8183106146e55750509060206146b892820101386146ab565b60209193508060019154838589010152019101909184926146cc565b602093506146b89592507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0091501682840152151560051b820101386146ab565b67ffffffffffffffff166000526008602052613e0d600460406000200161467f565b91908110156145fd5760081b0190565b358015158103613ae35790565b3561ffff81168103613ae35790565b3563ffffffff81168103613ae35790565b359063ffffffff82168203613ae357565b359061ffff82168203613ae357565b91908110156145fd5760051b0190565b35906fffffffffffffffffffffffffffffffff82168203613ae357565b9190826060910312613ae3576040516060810181811067ffffffffffffffff821117613cc957604052604061484081839561482781613ca0565b8552614835602082016147d0565b6020860152016147d0565b910152565b6fffffffffffffffffffffffffffffffff6148836040809361486681613ca0565b1515865283614877602083016147d0565b166020870152016147d0565b16910152565b818110614894575050565b60008155600101614889565b80518015614910576020036148d2578051602082810191830183900312613ae357519060ff82116148d2575060ff1690565b612082906040519182917f953576f7000000000000000000000000000000000000000000000000000000008352602060048401526024830190613c41565b50507f000000000000000000000000000000000000000000000000000000000000000090565b9060ff8091169116039060ff8211613f0c57565b60ff16604d8111613f0c57600a0a90565b907f00000000000000000000000000000000000000000000000000000000000000009060ff82169060ff811692828414614a6157828411614a3757906149a091614936565b91604d60ff84161180156149fe575b6149c8575050906149c2613e0d9261494a565b90613ef9565b9091507fa9cb113d0000000000000000000000000000000000000000000000000000000060005260045260245260445260646000fd5b50614a088361494a565b8015613f45577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0484116149af565b614a4091614936565b91604d60ff8416116149c857505090614a5b613e0d9261494a565b90613f3b565b5050505090565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115614b4a57614a9b8161519a565b7dffff00000000000000000000000000000000000000000000000000000000601082811c9085901c1616614b4a5761ffff8360e01c168015918215614b39575b5050614ae5575050565b7fffffffff0000000000000000000000000000000000000000000000000000000092507fdf63778f000000000000000000000000000000000000000000000000000000006000526004521660245260446000fd5b60e01c61ffff161090503880614adb565b505050565b614b576143f8565b506fffffffffffffffffffffffffffffffff6060820151166fffffffffffffffffffffffffffffffff8083511691614bb46020850193614bae614ba163ffffffff87511642613f74565b8560808901511690613ef9565b9061518d565b80821015614bcd57505b16825263ffffffff4216905290565b9050614bbe565b90816020910312613ae357518015158103613ae35790565b73ffffffffffffffffffffffffffffffffffffffff600154163303614c0d57565b7f2b5c74de0000000000000000000000000000000000000000000000000000000060005260046000fd5b90805115614e775767ffffffffffffffff81516020830120921691826000526008602052614c6c8160056040600020016157f1565b15614e335760005260096020526040600020815167ffffffffffffffff8111613cc957614c99825461462c565b601f8111614e01575b506020601f8211600114614d3b5791614d15827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea9593614d2b95600091614d30575b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b9055604051918291602083526020830190613c41565b0390a2565b905084015138614ce4565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe082169083600052806000209160005b818110614de9575092614d2b9492600192827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea989610614db2575b5050811b019055611d30565b8501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690553880614da6565b9192602060018192868a015181550194019201614d6b565b614e2d90836000526020600020601f840160051c81019160208510610f2a57601f0160051c0190614889565b38614ca2565b50906120826040519283927f393b8ad20000000000000000000000000000000000000000000000000000000084526004840152604060248401526044830190613c41565b7f14c880ca0000000000000000000000000000000000000000000000000000000060005260046000fd5b60405160ff7f000000000000000000000000000000000000000000000000000000000000000016602082015260208152613e0d604082613d4c565b81519192911561505e576fffffffffffffffffffffffffffffffff6040840151166fffffffffffffffffffffffffffffffff60208501511610614ffb576146b891925b805182547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff0000000000000000000000000000000000000000161782556020810151825460409290920151608090811b7fffffffffffffffffffffffffffffffff00000000000000000000000000000000166fffffffffffffffffffffffffffffffff9290921691821760018501557fffffffffffffffffffffffff0000000000000000000000000000000000000000909216174290911b73ffffffff0000000000000000000000000000000016179055565b60648361505c604051917f8020d12400000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565bfd5b6fffffffffffffffffffffffffffffffff604084015116158015906150ed575b61508c576146b89192614f1f565b60648361505c604051917fd68af9cc00000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b506fffffffffffffffffffffffffffffffff602084015116151561507e565b906127109167ffffffffffffffff6151266020830161457e565b166000908152600b60205260409020917fffffffff00000000000000000000000000000000000000000000000000000000161561517757606061ffff615173935460901c16910135613ef9565b0490565b606061ffff615173935460801c16910135613ef9565b91908201809211613f0c57565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115615271577dffff000000000000000000000000000000000000000000000000000000008116156152685760ff60015b169060f01c80615232575b506001036152055750565b7fc512f96c0000000000000000000000000000000000000000000000000000000060005260045260246000fd5b60005b6010811061524357506151fa565b6001811b8216615256575b600101615235565b9160018101809111613f0c579161524e565b60ff60006151ef565b5050565b9167ffffffffffffffff7f50f6fbee3ceedce6b7fd7eaef18244487867e6718aec7208187efb6b7908c14c9216928360005260086020526152be81836002604060002001615846565b6040805173ffffffffffffffffffffffffffffffffffffffff909216825260208201929092529081908101614d2b565b91909167ffffffffffffffff83169283600052600560205260ff60406000205460a01c16156153535750907fc6735cd4fa2bbe7b203b1682936e6ee61bc1702464bbbd12abb6630229d9a5f9918360005260056020526152be81836040600020615846565b906146b89350615275565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe182360301811215613ae357016020813591019167ffffffffffffffff8211613ae3578136038313613ae357565b9167ffffffffffffffff7fff0133389f9bb82d5b9385826160eaf2328039f6fa950eeb8cf0836da81789449216928360005260086020526152be81836040600020615846565b91909167ffffffffffffffff83169283600052600460205260ff60406000205460a01c16156154595750907f28d6c52e2b0b7587b0d195539fbe6af984b28791aca4d2cc0844244e38bce29e918360005260046020526152be81836040600020615846565b906146b893506153ae565b906040519182815491828252602082019060005260206000209260005b8181106154965750506146b892500383613d4c565b8454835260019485019487945060209093019201615481565b80548210156145fd5760005260206000200190600090565b6000818152600760205260409020548015615656577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613f0c57600654907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613f0c578181036155e7575b50505060065480156155b8577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff016155758160066154af565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b19169055600655600052600760205260006040812055600190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b61563e6155f86156099360066154af565b90549060031b1c92839260066154af565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b9055600052600760205260406000205538808061553c565b5050600090565b9060018201918160005282602052604060002054801515600014615788577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613f0c578254907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613f0c57818103615751575b505050805480156155b8577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff019061571282826154af565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b191690555560005260205260006040812055600190565b61577161576161560993866154af565b90549060031b1c928392866154af565b9055600052836020526040600020553880806156da565b50505050600090565b806000526007602052604060002054156000146157eb5760065468010000000000000000811015613cc9576157d261560982600185940160065560066154af565b9055600654906000526007602052604060002055600190565b50600090565b60008281526001820160205260409020546156565780549068010000000000000000821015613cc9578261582f6156098460018096018555846154af565b905580549260005201602052604060002055600190565b9182549060ff8260a01c16158015615afa575b615af4576fffffffffffffffffffffffffffffffff8216916001850190815461589e63ffffffff6fffffffffffffffffffffffffffffffff83169360801c1642613f74565b9081615a56575b5050848110615a0a57508383106158ff5750506158d46fffffffffffffffffffffffffffffffff928392613f74565b16167fffffffffffffffffffffffffffffffff00000000000000000000000000000000825416179055565b9190915460801c92831561599e578161591791613f74565b927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810190808211613f0c5761596561596a9273ffffffffffffffffffffffffffffffffffffffff9661518d565b613f3b565b7fd0c8d23a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b73ffffffffffffffffffffffffffffffffffffffff83837fd0c8d23a000000000000000000000000000000000000000000000000000000006000527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6004526024521660445260646000fd5b828573ffffffffffffffffffffffffffffffffffffffff927f1a76572a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b828692939611615aca57615a7192614bae9160801c90613ef9565b80841015615ac55750825b85547fffffffffffffffffffffffff00000000ffffffffffffffffffffffffffffffff164260801b73ffffffff00000000000000000000000000000000161786559238806158a5565b615a7c565b7f9725942a0000000000000000000000000000000000000000000000000000000060005260046000fd5b50505050565b50821561585956fea164736f6c634300081a000a' as const +// generate:end diff --git a/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/burn-with-from-mint-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/burn-with-from-mint-token-pool.ts new file mode 100644 index 00000000..b109a687 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/burn-with-from-mint-token-pool.ts @@ -0,0 +1,4 @@ +export default // generate: +// `'${require('fs').readFileSync(require.resolve('@chainlink/contracts-ccip/bytecode/v2_0_0/burn_with_from_mint_token_pool.bin'), 'utf8').trim()}' as const` +'0x60e080604052346102aa5760a081615ee1803803809161001f82856102fb565b8339810103126102aa5780516001600160a01b03811691908290036102aa5761004a60208201610334565b61005660408301610342565b9061006f608061006860608601610342565b9401610342565b9233156102ea57600180546001600160a01b03191633179055841580156102d9575b80156102c8575b6102b7578460805260c052308403610220575b60a052600380546001600160a01b039283166001600160a01b0319918216179091556002805493909216921691909117905560405163095ea7b360e01b60208083019182523060248401526000196044808501919091528352906000906101136064856102fb565b83519082865af16000513d82610204575b5050156101bf575b604051615b2390816103be823960805181818161023e01528181610491015281816122700152818161244801528181612ab501528181612cb0015281816131a20152818161374f01526137a9015260a05181818161361501528181614928015281816149720152614ebc015260c0518181816102d9015281816113f50152818161230a01528181612b50015261323d0152f35b6101fd916101f860405163095ea7b360e01b602082015230602482015260006044820152604481526101f26064826102fb565b82610356565b610356565b388061012c565b9091506102185750813b15155b3880610124565b600114610211565b60405163313ce56760e01b8152602081600481885afa60009181610276575b5061024b575b506100ab565b60ff1660ff821681810361025f5750610245565b6332ad3e0760e11b60005260045260245260446000fd5b9091506020813d6020116102af575b81610292602093836102fb565b810103126102aa576102a390610334565b903861023f565b600080fd5b3d9150610285565b630a64406560e11b60005260046000fd5b506001600160a01b03811615610098565b506001600160a01b03841615610091565b639b15e16f60e01b60005260046000fd5b601f909101601f19168101906001600160401b0382119082101761031e57604052565b634e487b7160e01b600052604160045260246000fd5b519060ff821682036102aa57565b51906001600160a01b03821682036102aa57565b906000602091828151910182855af1156103b1576000513d6103a857506001600160a01b0381163b155b6103875750565b635274afe760e01b60009081526001600160a01b0391909116600452602490fd5b60011415610380565b6040513d6000823e3d90fdfe608080604052600436101561001357600080fd5b600090813560e01c90816301ffc9a7146139ca5750806306b859ef146138e5578063181f5a77146138845780631826b1e7146137cd57806321df0da71461377c578063240028e8146137185780632422ac451461363957806324f65ee7146135fb5780632cab0fb61461310757806337a3210d146130d35780633907753714612a0a5780634c5ef0ed146129c357806362ddd3c41461293c5780637437ff9f146128ee57806379ba5097146128275780638926f54f146127e15780638da5cb5b146127ad5780639a4575b9146121f7578063a42a7b8b14612090578063acfecf9114611f98578063ae39a25714611e0d578063b6cfa3b714611d52578063b794658014611d1a578063bfeffd3f14611c6e578063c4bffe2b14611b43578063c7230a601461189d578063dc04fa1f14611419578063dc0bd971146113c8578063dcbd41bc146111c4578063e8a1da1714610ae8578063ea6396db146109aa578063ec6ae7a714610967578063f2fde38b146108985763fbc801a71461019757600080fd5b346105db5760606003193601126105db576004359067ffffffffffffffff82116105db578160040160a060031984360301126105e9576101d5613afc565b9060443567ffffffffffffffff811161070f57906101fa610217923690600401613c27565b92906102046145e4565b5061020f8584615120565b933691613da1565b92608486019361022685614571565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361084e57602487019677ffffffffffffffff0000000000000000000000000000000061028c89614592565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156107c157889161081f575b506107f75767ffffffffffffffff61032089614592565b16610338816000526007602052604060002054151590565b156107cc57602073ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156107c1578890610770575b73ffffffffffffffffffffffffffffffffffffffff9150163303610744576064810135936103c78686613f88565b7fffffffff00000000000000000000000000000000000000000000000000000000851694851561072257610423907fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1690614a7c565b61043f816104308a614571565b6104398d614592565b90615408565b73ffffffffffffffffffffffffffffffffffffffff6003541693846105ed575b5050505050509061046f91613f88565b9161047984614592565b5073ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b156105e9576040517f9dc29fac000000000000000000000000000000000000000000000000000000008152306004820152602481018590529082908290604490829084905af180156105de576105c6575b6105bc8461058b61058688877ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff61054c61054685614592565b93614571565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252336020830152810188905292169180606081015b0390a2614592565b614755565b90610594614eb5565b604051926105a184613d0c565b83526020830152604051928392604084526040840190613e69565b9060208301520390f35b6105d1828092613d60565b6105db5780610504565b80fd5b6040513d84823e3d90fd5b5080fd5b843b1561071e578994928b9694928692604051988997889687957fa8027c0f00000000000000000000000000000000000000000000000000000000875260048701608090528061063c91615372565b6084880160a0905261012488019061065392613fb6565b9261065d90613c12565b67ffffffffffffffff1660a487015260440161067890613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660c48601528d8c60e48701526106a390613bc3565b73ffffffffffffffffffffffffffffffffffffffff1661010486015260248501528381036003190160448501526106d991613c55565b90606483015203925af18015610713579085916106fa575b8080808061045f565b8161070491613d60565b61070f5783386106f1565b8380fd5b6040513d87823e3d90fd5b8980fd5b5061073f816107308a614571565b6107398d614592565b906153c2565b61043f565b6024877f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b506020813d6020116107b9575b8161078a60209383613d60565b810103126107b5576107b073ffffffffffffffffffffffffffffffffffffffff91613f95565b610399565b8780fd5b3d915061077d565b6040513d8a823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008852600452602487fd5b6004877f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b610841915060203d602011610847575b6108398183613d60565b810190614be8565b38610309565b503d61082f565b60248673ffffffffffffffffffffffffffffffffffffffff61086f88614571565b7f961c9a4f00000000000000000000000000000000000000000000000000000000835216600452fd5b50346105db5760206003193601126105db5773ffffffffffffffffffffffffffffffffffffffff6108c7613b5a565b6108cf614c00565b1633811461093f57807fffffffffffffffffffffffff000000000000000000000000000000000000000083541617825573ffffffffffffffffffffffffffffffffffffffff600154167fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12788380a380f35b6004827fdad89dca000000000000000000000000000000000000000000000000000000008152fd5b50346105db57806003193601126105db5760207fffffffff0000000000000000000000000000000000000000000000000000000060025460401b16604051908152f35b50346105db5760806003193601126105db576109c4613b5a565b506109cd613be4565b6109d5613b2b565b5060643567ffffffffffffffff8111610ae4579167ffffffffffffffff604092610a0560e0953690600401613c27565b50508260c08551610a1581613d44565b82815282602082015282878201528260608201528260808201528260a08201520152168152600b6020522060405190610a4d82613d44565b5461ffff818163ffffffff82169485815263ffffffff60208201818560201c1681528160408401818760401c168152816060860193818960601c16855260ff60c060808901988a8c60801c168a528a60a082019c60901c168c52019b60a01c1615158b526040519b8c52511660208b0152511660408901525116606087015251166080850152511660a083015251151560c0820152f35b8280fd5b50346105db5760406003193601126105db5760043567ffffffffffffffff81116105e957610b1a903690600401613e93565b9060243567ffffffffffffffff811161070f5790610b3d84923690600401613e93565b939091610b48614c00565b83905b8282106110055750505081927ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee182360301935b81811015611001578060051b83013585811215610ffd57830161012081360312610ffd5760405194610baf86613d28565b610bb882613c12565b8652602082013567ffffffffffffffff81116105e95782019436601f870112156105e957853595610be887613ef5565b96610bf66040519889613d60565b80885260208089019160051b83010190368211610ffd5760208301905b828210610fca575050505060208701958652604083013567ffffffffffffffff8111610ae457610c469036908501613e06565b9160408801928352610c70610c5e3660608701614801565b9460608a0195865260c0369101614801565b956080890196875283515115610fa257610c9467ffffffffffffffff8a51166157a5565b15610f6b5767ffffffffffffffff8951168252600860205260408220610cbb865182614ef0565b610cc9885160028301614ef0565b6004855191019080519067ffffffffffffffff8211610f3e57610cec8354614640565b601f8111610f03575b50602090601f8311600114610e6457610d439291869183610e59575b50507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b90555b815b88518051821015610d7d5790610d77600192610d708367ffffffffffffffff8f5116926145fd565b5190614c4b565b01610d48565b5050977f8d340f17e19058004c20453540862a9c62778504476f6756755cb33bcd6c38c2939199975095610e4b67ffffffffffffffff6001979694985116925193519151610e17610de260405196879687526101006020880152610100870190613c55565b9360408601906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b60a08401906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b0390a1019392909193610b7e565b015190508e80610d11565b83865281862091907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08416875b818110610eeb5750908460019594939210610eb4575b505050811b019055610d46565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690558d8080610ea7565b92936020600181928786015181550195019301610e91565b610f2e9084875260208720601f850160051c81019160208610610f34575b601f0160051c019061489d565b8d610cf5565b9091508190610f21565b6024857f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b60248267ffffffffffffffff8b51167f1d5ad3c5000000000000000000000000000000000000000000000000000000008252600452fd5b6004827f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b813567ffffffffffffffff8111610ff957602091610fee8392833691890101613e06565b815201910190610c13565b8680fd5b8480fd5b8380f35b9267ffffffffffffffff6110276110228486889a9699979a6147d4565b614592565b1691611032836154db565b1561119857828452600860205261104e60056040862001615478565b94845b865181101561108757600190858752600860205261108060056040892001611079838b6145fd565b5190615671565b5001611051565b50939692909450949094808752600860205260056040882088815588600182015588600282015588600382015588600482016110c38154614640565b80611157575b5050500180549088815581611139575b5050907f5204aec90a3c794d8e90fded8b46ae9c7c552803e7e832e0c1d358396d8599166020836001948a52600482528985604082208281550155808a52600582528985604082208281550155604051908152a101909194939294610b4b565b885260208820908101905b818110156110d957888155600101611144565b601f811160011461116d5750555b888a806110c9565b8183526020832061118891601f01861c81019060010161489d565b8082528160208120915555611165565b602484847f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b50346105db5760206003193601126105db5760043567ffffffffffffffff81116105e9576111f6903690600401613ec4565b73ffffffffffffffffffffffffffffffffffffffff600a5416331415806113a6575b61137a57825b818110611229578380f35b611234818385614777565b67ffffffffffffffff61124682614592565b169061125f826000526007602052604060002054151590565b1561134e57907f41f7c8f7cfdad9350aa495e6c54cbbf750a07ab38a9098aed1256e30dd1682bb60e08361130e6112e8602060019897018b6112a082614787565b156113155787905260046020526112c760408d206112c13660408801614801565b90614ef0565b868c5260056020526112e360408d206112c13660a08801614801565b614787565b9160405192151583526113016020840160408301614859565b60a0608084019101614859565ba20161121e565b60026040828a6112e3945260086020526113378282206112c136858c01614801565b8a8152600860205220016112c13660a08801614801565b602486837f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b6024837f8e4a23d600000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff60015416331415611218565b50346105db57806003193601126105db57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105db5760406003193601126105db5760043567ffffffffffffffff81116105e95761144b903690600401613ec4565b60243567ffffffffffffffff811161070f5761146b903690600401613e93565b919092611476614c00565b845b8281106114e257505050825b81811061148f578380f35b8067ffffffffffffffff6114a961102260019486886147d4565b16808652600b6020528560408120557f5479bbc0288b7eaeaf2ace0943b88016cc648964fcd42919a86fd93b15fdbee88680a201611484565b67ffffffffffffffff6114f9611022838686614777565b16611511816000526007602052604060002054151590565b1561187257611521828585614777565b602081019060e081019061153482614787565b156118465760a0810161271061ffff61154c83614794565b1610156118375760c082019161271061ffff61156785614794565b1610156117ff5763ffffffff61157c866147a3565b16156117d357858c52600b60205260408c20611597866147a3565b63ffffffff169080549060408401916115af836147a3565b60201b67ffffffff00000000169360608601946115cb866147a3565b60401b6bffffffff00000000000000001696608001966115ea886147a3565b60601b6fffffffff00000000000000000000000016916116098a614794565b60801b71ffff00000000000000000000000000000000169361162a8c614794565b60901b73ffff00000000000000000000000000000000000016957fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016177fffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffff16177fffffffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffff16177fffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffffffff1617171781556116dd87614787565b81547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff0000000000000000000000000000000000000000161790556040519661172e906147b4565b63ffffffff16875261173f906147b4565b63ffffffff166020870152611753906147b4565b63ffffffff166040860152611767906147b4565b63ffffffff16606085015261177b906147c5565b61ffff16608084015261178d906147c5565b61ffff1660a083015261179f90613cb4565b151560c082015260e07ffae1e296719dac5269c3886fb5002bb29bf17ae403060c6eb063a55abaaa104191a2600101611478565b60248c877f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b60248c61ffff61180e86614794565b7f95f3517a00000000000000000000000000000000000000000000000000000000835216600452fd5b8a61ffff61180e602493614794565b60248a857f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b7f1e670e4b000000000000000000000000000000000000000000000000000000008752600452602486fd5b50346105db5760406003193601126105db5760043567ffffffffffffffff81116105e9576118cf903690600401613e93565b906118d8613ba0565b9173ffffffffffffffffffffffffffffffffffffffff6001541633141580611b21575b611af55773ffffffffffffffffffffffffffffffffffffffff8316908115611acd57845b81811061192a578580f35b73ffffffffffffffffffffffffffffffffffffffff61195261194d8385886147d4565b614571565b166040517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152602081602481855afa9081156107c1578891611a9a575b50806119a7575b505060010161191f565b6040517fa9059cbb00000000000000000000000000000000000000000000000000000000602080830191825273ffffffffffffffffffffffffffffffffffffffff8a16602484015260448084018590528352918a9190611a08606482613d60565b519082865af115611a8f5787513d611a865750813b155b611a5a5790847f508d7d183612c18fc339b42618912b9fa3239f631dd7ec0671f950200a0fa66e602060019594604051908152a3903861199d565b602488837f5274afe7000000000000000000000000000000000000000000000000000000008252600452fd5b60011415611a1f565b6040513d89823e3d90fd5b905060203d8111611ac6575b611ab08183613d60565b602082600092810103126105db57505138611996565b503d611aa6565b6004857f8579befe000000000000000000000000000000000000000000000000000000008152fd5b6024847fcb1afbd700000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff600c54163314156118fb565b50346105db57806003193601126105db57604051906006548083528260208101600684526020842092845b818110611c55575050611b8392500383613d60565b8151611ba7611b9182613ef5565b91611b9f6040519384613d60565b808352613ef5565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0602083019301368437805b8451811015611c06578067ffffffffffffffff611bf3600193886145fd565b5116611bff82866145fd565b5201611bd4565b50925090604051928392602084019060208552518091526040840192915b818110611c32575050500390f35b825167ffffffffffffffff16845285945060209384019390920191600101611c24565b8454835260019485019487945060209093019201611b6e565b50346105db5760206003193601126105db5760043573ffffffffffffffffffffffffffffffffffffffff81168091036105e957611ca9614c00565b7fffffffffffffffffffffffff00000000000000000000000000000000000000006003547fbaff46844acf36d6ee996f489a1a288709c4542bd33cd557770afd267d5812096040805173ffffffffffffffffffffffffffffffffffffffff84168152856020820152a1161760035580f35b50346105db5760206003193601126105db57611d4e611d3a610586613bfb565b604051918291602083526020830190613c55565b0390f35b50346105db5760206003193601126105db577f307cf716eade81675bea3ccb6917b0f91baa2160056765d9a83d76f819caf06a6020611d8f613ac8565b611d97614c00565b6002547fffffffffffffffff00000000ffffffffffffffffffffffffffffffffffffffff77ffffffff0000000000000000000000000000000000000000808460401c16169116176002557fffffffff0000000000000000000000000000000000000000000000000000000060405191168152a180f35b50346105db5760606003193601126105db57611e27613b5a565b90611e30613ba0565b6044359273ffffffffffffffffffffffffffffffffffffffff841680850361070f57611e5a614c00565b73ffffffffffffffffffffffffffffffffffffffff82168015611f705794611f6a917f3f1036e85d016a93254a0b1415844f79b85424959d90ae5ad51ce8f4533fe70195967fffffffffffffffffffffffff0000000000000000000000000000000000000000600254161760025573ffffffffffffffffffffffffffffffffffffffff85167fffffffffffffffffffffffff0000000000000000000000000000000000000000600a541617600a557fffffffffffffffffffffffff0000000000000000000000000000000000000000600c541617600c556040519384938491604091949373ffffffffffffffffffffffffffffffffffffffff809281606087019816865216602085015216910152565b0390a180f35b6004857f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b50346105db5767ffffffffffffffff611fb036613e24565b929091611fbb614c00565b1691611fd4836000526007602052604060002054151590565b1561119857828452600860205261200360056040862001611ff6368486613da1565b6020815191012090615671565b1561204857907f52d00ee4d9bd51b40168f2afc5848837288ce258784ad914278791464b3f4d7691612042604051928392602084526020840191613fb6565b0390a280f35b8261208c836040519384937f74f23c7c0000000000000000000000000000000000000000000000000000000085526004850152604060248501526044840191613fb6565b0390fd5b50346105db5760206003193601126105db5767ffffffffffffffff6120b3613bfb565b16815260086020526120ca60056040832001615478565b80517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061210f6120f983613ef5565b926121076040519485613d60565b808452613ef5565b01835b8181106121e6575050825b82518110156121635780612133600192856145fd565b518552600960205261214760408620614693565b61215182856145fd565b5261215c81846145fd565b500161211d565b81846040519182916020830160208452825180915260408401602060408360051b870101940192905b82821061219b57505050500390f35b919360206121d6827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc060019597998495030186528851613c55565b960192019201859493919261218c565b806060602080938601015201612112565b50346105db5760206003193601126105db5760043567ffffffffffffffff81116105e957806004019060a06003198236030112610ae4576122366145e4565b506040516020936122478583613d60565b808252608483019161225883614571565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361278c57602484019477ffffffffffffffff000000000000000000000000000000006122be87614592565b60801b16604051907f2cbc26bb0000000000000000000000000000000000000000000000000000000082526004820152878160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa90811561271157849161276f575b506127475767ffffffffffffffff61235187614592565b16612369816000526007602052604060002054151590565b1561271c578773ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156127115784906126c9575b73ffffffffffffffffffffffffffffffffffffffff915016330361269d57606485013594612403866123fa87614571565b6107398a614592565b73ffffffffffffffffffffffffffffffffffffffff600354169182612580575b5050505061243084614592565b5073ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b156105e9576040517f9dc29fac000000000000000000000000000000000000000000000000000000008152306004820152602481018590529082908290604490829084905af180156105de5761256b575b8561253b61058687877ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff8961057e6125046124fe87614592565b92614571565b6040805173ffffffffffffffffffffffffffffffffffffffff90921682523360208301528101959095529116929081906060820190565b90612544614eb5565b6040519261255184613d0c565b835281830152611d4e604051928284938452830190613e69565b612576828092613d60565b6105db57806124bb565b823b15610ffd57918791858094604051968795869485937fa8027c0f0000000000000000000000000000000000000000000000000000000085526004850160809052806125cc91615372565b6084860160a090526101248601906125e392613fb6565b916125ed90613c12565b67ffffffffffffffff1660a485015260440161260890613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660c48401528b60e48401526126328b613bc3565b73ffffffffffffffffffffffffffffffffffffffff1661010484015283602484015282810360031901604484015261266991613c55565b8a606483015203925af180156105de57908291612688575b8080612423565b8161269291613d60565b6105db578038612681565b6024837f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b508781813d831161270a575b6126df8183613d60565b8101031261070f5761270573ffffffffffffffffffffffffffffffffffffffff91613f95565b6123c9565b503d6126d5565b6040513d86823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008452600452602483fd5b6004837f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b6127869150883d8a11610847576108398183613d60565b3861233a565b5073ffffffffffffffffffffffffffffffffffffffff61086f602493614571565b50346105db57806003193601126105db57602073ffffffffffffffffffffffffffffffffffffffff60015416604051908152f35b50346105db5760206003193601126105db57602061281d67ffffffffffffffff612809613bfb565b166000526007602052604060002054151590565b6040519015158152f35b50346105db57806003193601126105db57805473ffffffffffffffffffffffffffffffffffffffff811633036128c6577fffffffffffffffffffffffff000000000000000000000000000000000000000060015491338284161760015516825573ffffffffffffffffffffffffffffffffffffffff3391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08380a380f35b6004827f02b543c6000000000000000000000000000000000000000000000000000000008152fd5b50346105db57806003193601126105db57600254600a54600c546040805173ffffffffffffffffffffffffffffffffffffffff94851681529284166020840152921691810191909152606090f35b50346105db5761294b36613e24565b61295793929193614c00565b67ffffffffffffffff8216612979816000526007602052604060002054151590565b156129985750612995929361298f913691613da1565b90614c4b565b80f35b7f1e670e4b000000000000000000000000000000000000000000000000000000008452600452602483fd5b50346105db5760406003193601126105db576129dd613bfb565b906024359067ffffffffffffffff82116105db57602061281d84612a043660048701613e06565b906145a7565b50346105db5760206003193601126105db576004359067ffffffffffffffff82116105db57816004019061010060031984360301126105db5780604051612a5081613cc1565b5280604051612a5e81613cc1565b52606483013560c4840193612a8e612a88612a83612a7c8888614520565b3691613da1565b6148b4565b8361496f565b936084820195612a9d87614571565b73ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116036130b257602483019377ffffffffffffffff00000000000000000000000000000000612b0386614592565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115611a8f578791613093575b5061306b5767ffffffffffffffff612b9786614592565b16612baf816000526007602052604060002054151590565b1561304057602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa908115611a8f578791613021575b5015612ff557612c2685614592565b92612c3c60a4860194612a04612a7c8785614520565b15612fae57612c5d88612c4e8b614571565b612c5789614592565b90615289565b73ffffffffffffffffffffffffffffffffffffffff600354169283612de0575b505050505060440191612c8f83614571565b612c9883614592565b5073ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001690813b15610ae4576040517f40c10f1900000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff919091166004820152602481018690529082908290604490829084905af180156105de57612dcb575b5050608067ffffffffffffffff60209573ffffffffffffffffffffffffffffffffffffffff612d97612d916105467ffc5e3a5bddc11d92c2dc20fae6f7d5eb989f056be35239f7de7e86150609abc097614592565b96614571565b816040519716875233898801521660408601528560608601521692a260405190612dc082613cc1565b815260405190518152f35b612dd6828092613d60565b6105db5780612d3c565b833b156107b557878795938195938c93604051988997889687957f6371157400000000000000000000000000000000000000000000000000000000875260048701606090528d612e308780615372565b60648a0161010090526101648a0190612e4892613fb6565b94612e5290613c12565b67ffffffffffffffff166084890152604401612e6d90613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660a488015260c4870152612e9690613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660e4860152612ebb9084615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c86840301610104870152612ef09291613fb6565b90612efb9083615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c85840301610124860152612f309291613fb6565b9060e48a01612f3e91615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c84840301610144850152612f739291613fb6565b8b602483015282604483015203925af1801561271157908491612f99575b808080612c7d565b81612fa391613d60565b610ae4578238612f91565b83612fb891614520565b61208c6040519283927f24eb47e5000000000000000000000000000000000000000000000000000000008452602060048501526024840191613fb6565b6024867f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b61303a915060203d602011610847576108398183613d60565b38612c17565b7fa9902c7e000000000000000000000000000000000000000000000000000000008752600452602486fd5b6004867f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b6130ac915060203d602011610847576108398183613d60565b38612b80565b60248573ffffffffffffffffffffffffffffffffffffffff61086f8a614571565b50346105db57806003193601126105db57602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b50346105db5760406003193601126105db576004359067ffffffffffffffff82116105db57816004019061010060031984360301126105db57613148613afc565b918160405161315681613cc1565b5260648401359360c481019361317b613175612a83612a7c8887614520565b8761496f565b94608483019661318a88614571565b73ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116036135da57602484019477ffffffffffffffff000000000000000000000000000000006131f087614592565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156107c15788916135bb575b506107f75767ffffffffffffffff61328487614592565b1661329c816000526007602052604060002054151590565b156107cc57602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa9081156107c157889161359c575b50156107445761331386614592565b9361332960a4870195612a04612a7c8886614520565b15613592577fffffffff000000000000000000000000000000000000000000000000000000001690811561357757613373896133648c614571565b61336d8a614592565b90615302565b73ffffffffffffffffffffffffffffffffffffffff6003541693846133a6575b50505050505060440191612c8f83614571565b843b1561357357868995938c959387938b6040519a8b998a9889977f6371157400000000000000000000000000000000000000000000000000000000895260048901606090526133f68780615372565b60648b0161010090526101648b019061340e92613fb6565b9461341890613c12565b67ffffffffffffffff1660848a015260440161343390613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660a489015260c488015261345c90613bc3565b73ffffffffffffffffffffffffffffffffffffffff1660e48701526134819084615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c878403016101048801526134b69291613fb6565b906134c19083615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c868403016101248701526134f69291613fb6565b9060e48b0161350491615372565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c858403016101448601526135399291613fb6565b908c6024840152604483015203925af180156127115761355e575b8080808080613393565b9261356c8160449395613d60565b9290613554565b8880fd5b61358d896135848c614571565b612c578a614592565b613373565b612fb88583614520565b6135b5915060203d602011610847576108398183613d60565b38613304565b6135d4915060203d602011610847576108398183613d60565b3861326d565b60248673ffffffffffffffffffffffffffffffffffffffff61086f8b614571565b50346105db57806003193601126105db57602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105db5760406003193601126105db57613653613bfb565b6024359182151583036105db57610140613716613670858561449d565b6136c660409392935180946fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565b60a08301906fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565bf35b50346105db5760206003193601126105db57602090613735613b5a565b905073ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116146040519015158152f35b50346105db57806003193601126105db57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346105db5760c06003193601126105db576137e7613b5a565b506137f0613be4565b6137f8613b7d565b50608435917fffffffff00000000000000000000000000000000000000000000000000000000831683036105db5760a4359067ffffffffffffffff82116105db5760a063ffffffff8061ffff61385d88886138563660048b01613c27565b50506142ed565b95926040979194975197885216602087015216604085015216606083015215156080820152f35b50346105db57806003193601126105db5750611d4e6040516138a7604082613d60565b601f81527f4275726e5769746846726f6d4d696e74546f6b656e506f6f6c20322e302e30006020820152604051918291602083526020830190613c55565b50346105db5760c06003193601126105db576138ff613b5a565b613907613be4565b906064357fffffffff000000000000000000000000000000000000000000000000000000008116810361070f5760843567ffffffffffffffff8111610ffd57613954903690600401613c27565b9160a435936002851015610ff95761396f9560443591613ff5565b90604051918291602083016020845282518091526020604085019301915b81811061399b575050500390f35b825173ffffffffffffffffffffffffffffffffffffffff1684528594506020938401939092019160010161398d565b9050346105e95760206003193601126105e9576020907fffffffff00000000000000000000000000000000000000000000000000000000613a09613ac8565b167faff2afbf000000000000000000000000000000000000000000000000000000008114908115613a9e575b8115613a74575b8115613a4a575b5015158152f35b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501483613a43565b7f0e64dd290000000000000000000000000000000000000000000000000000000081149150613a3c565b7f940a15420000000000000000000000000000000000000000000000000000000081149150613a35565b600435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613af757565b600080fd5b602435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613af757565b604435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613af757565b6004359073ffffffffffffffffffffffffffffffffffffffff82168203613af757565b6064359073ffffffffffffffffffffffffffffffffffffffff82168203613af757565b6024359073ffffffffffffffffffffffffffffffffffffffff82168203613af757565b359073ffffffffffffffffffffffffffffffffffffffff82168203613af757565b6024359067ffffffffffffffff82168203613af757565b6004359067ffffffffffffffff82168203613af757565b359067ffffffffffffffff82168203613af757565b9181601f84011215613af75782359167ffffffffffffffff8311613af75760208381860195010111613af757565b919082519283825260005b848110613c9f5750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8460006020809697860101520116010190565b80602080928401015182828601015201613c60565b35908115158203613af757565b6020810190811067ffffffffffffffff821117613cdd57604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040810190811067ffffffffffffffff821117613cdd57604052565b60a0810190811067ffffffffffffffff821117613cdd57604052565b60e0810190811067ffffffffffffffff821117613cdd57604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117613cdd57604052565b92919267ffffffffffffffff8211613cdd5760405191613de9601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200184613d60565b829481845281830111613af7578281602093846000960137010152565b9080601f83011215613af757816020613e2193359101613da1565b90565b906040600319830112613af75760043567ffffffffffffffff81168103613af757916024359067ffffffffffffffff8211613af757613e6591600401613c27565b9091565b613e21916020613e828351604084526040840190613c55565b920151906020818403910152613c55565b9181601f84011215613af75782359167ffffffffffffffff8311613af7576020808501948460051b010111613af757565b9181601f84011215613af75782359167ffffffffffffffff8311613af7576020808501948460081b010111613af757565b67ffffffffffffffff8111613cdd5760051b60200190565b81810292918115918404141715613f2057565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8115613f59570490565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b91908203918211613f2057565b519073ffffffffffffffffffffffffffffffffffffffff82168203613af757565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0938186528686013760008582860101520116010190565b92959390919473ffffffffffffffffffffffffffffffffffffffff600354169586156142cb578097600287101561429c5773ffffffffffffffffffffffffffffffffffffffff98614156957fffffffff0000000000000000000000000000000000000000000000000000000093896142725767ffffffffffffffff8216600052600b6020526040600020906040519161408d83613d44565b549163ffffffff8316815263ffffffff8360201c16602082015263ffffffff8360401c16604082015263ffffffff8360601c16606082015260c061ffff8460801c169182608082015260ff60a082019561ffff8160901c16875260a01c161515918291015261421e575b50505067ffffffffffffffff905b6040519b8c997f06b859ef000000000000000000000000000000000000000000000000000000008b521660048a0152166024880152604487015216606485015260c0608485015260c4840191613fb6565b928180600095869560a483015203915afa91821561421157819261417957505090565b9091503d8083833e61418b8183613d60565b810190602081830312610ae45780519067ffffffffffffffff821161070f570181601f82011215610ae4578051906141c282613ef5565b936141d06040519586613d60565b82855260208086019360051b8301019384116105db5750602001905b8282106141f95750505090565b6020809161420684613f95565b8152019101906141ec565b50604051903d90823e3d90fd5b92935067ffffffffffffffff928587161561425a575061271061424961ffff61425094511683613f0d565b0490613f88565b915b9038806140f7565b61426c92506142496127109183613f0d565b91614252565b67ffffffffffffffff91925061429690614290612a8336898b613da1565b9061496f565b91614105565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b50505050505050506040516142e1602082613d60565b60008152600036813790565b67ffffffffffffffff9092919261432b7fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1685614a7c565b16600052600b60205260406000206040519061434682613d44565b549163ffffffff83169384835263ffffffff8460201c169384602085015263ffffffff8160401c169182604086015263ffffffff8260601c169081606087015261ffff8360801c169586608082015260ff61ffff8560901c16948560a084015260a01c16159060c082159101526143f3577fffffffff00000000000000000000000000000000000000000000000000000000166143e857505093929190600190565b959493509160019150565b5050505092505050600090600090600090600090600090565b6040519061441982613d28565b60006080838281528260208201528260408201528260608201520152565b9060405161444481613d28565b60806001829460ff81546fffffffffffffffffffffffffffffffff8116865263ffffffff81861c16602087015260a01c161515604085015201546fffffffffffffffffffffffffffffffff81166060840152811c910152565b67ffffffffffffffff916144af61440c565b506144b861440c565b506144ec57166000526008602052604060002090613e216144e060026144e56144e086614437565b614b63565b9401614437565b16908160005260046020526145076144e06040600020614437565b916000526005602052613e216144e06040600020614437565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215613af7570180359067ffffffffffffffff8211613af757602001918136038313613af757565b3573ffffffffffffffffffffffffffffffffffffffff81168103613af75790565b3567ffffffffffffffff81168103613af75790565b9067ffffffffffffffff613e2192166000526008602052600560406000200190602081519101209060019160005201602052604060002054151590565b604051906145f182613d0c565b60606020838281520152565b80518210156146115760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90600182811c92168015614689575b602083101461465a57565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b91607f169161464f565b90604051918260008254926146a784614640565b808452936001811690811561471557506001146146ce575b506146cc92500383613d60565b565b90506000929192526020600020906000915b8183106146f95750509060206146cc92820101386146bf565b60209193508060019154838589010152019101909184926146e0565b602093506146cc9592507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0091501682840152151560051b820101386146bf565b67ffffffffffffffff166000526008602052613e216004604060002001614693565b91908110156146115760081b0190565b358015158103613af75790565b3561ffff81168103613af75790565b3563ffffffff81168103613af75790565b359063ffffffff82168203613af757565b359061ffff82168203613af757565b91908110156146115760051b0190565b35906fffffffffffffffffffffffffffffffff82168203613af757565b9190826060910312613af7576040516060810181811067ffffffffffffffff821117613cdd57604052604061485481839561483b81613cb4565b8552614849602082016147e4565b6020860152016147e4565b910152565b6fffffffffffffffffffffffffffffffff6148976040809361487a81613cb4565b151586528361488b602083016147e4565b166020870152016147e4565b16910152565b8181106148a8575050565b6000815560010161489d565b80518015614924576020036148e6578051602082810191830183900312613af757519060ff82116148e6575060ff1690565b61208c906040519182917f953576f7000000000000000000000000000000000000000000000000000000008352602060048401526024830190613c55565b50507f000000000000000000000000000000000000000000000000000000000000000090565b9060ff8091169116039060ff8211613f2057565b60ff16604d8111613f2057600a0a90565b907f00000000000000000000000000000000000000000000000000000000000000009060ff82169060ff811692828414614a7557828411614a4b57906149b49161494a565b91604d60ff8416118015614a12575b6149dc575050906149d6613e219261495e565b90613f0d565b9091507fa9cb113d0000000000000000000000000000000000000000000000000000000060005260045260245260445260646000fd5b50614a1c8361495e565b8015613f59577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0484116149c3565b614a549161494a565b91604d60ff8416116149dc57505090614a6f613e219261495e565b90613f4f565b5050505090565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115614b5e57614aaf816151ae565b7dffff00000000000000000000000000000000000000000000000000000000601082811c9085901c1616614b5e5761ffff8360e01c168015918215614b4d575b5050614af9575050565b7fffffffff0000000000000000000000000000000000000000000000000000000092507fdf63778f000000000000000000000000000000000000000000000000000000006000526004521660245260446000fd5b60e01c61ffff161090503880614aef565b505050565b614b6b61440c565b506fffffffffffffffffffffffffffffffff6060820151166fffffffffffffffffffffffffffffffff8083511691614bc86020850193614bc2614bb563ffffffff87511642613f88565b8560808901511690613f0d565b906151a1565b80821015614be157505b16825263ffffffff4216905290565b9050614bd2565b90816020910312613af757518015158103613af75790565b73ffffffffffffffffffffffffffffffffffffffff600154163303614c2157565b7f2b5c74de0000000000000000000000000000000000000000000000000000000060005260046000fd5b90805115614e8b5767ffffffffffffffff81516020830120921691826000526008602052614c80816005604060002001615805565b15614e475760005260096020526040600020815167ffffffffffffffff8111613cdd57614cad8254614640565b601f8111614e15575b506020601f8211600114614d4f5791614d29827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea9593614d3f95600091614d44575b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b9055604051918291602083526020830190613c55565b0390a2565b905084015138614cf8565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe082169083600052806000209160005b818110614dfd575092614d3f9492600192827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea989610614dc6575b5050811b019055611d3a565b8501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690553880614dba565b9192602060018192868a015181550194019201614d7f565b614e4190836000526020600020601f840160051c81019160208510610f3457601f0160051c019061489d565b38614cb6565b509061208c6040519283927f393b8ad20000000000000000000000000000000000000000000000000000000084526004840152604060248401526044830190613c55565b7f14c880ca0000000000000000000000000000000000000000000000000000000060005260046000fd5b60405160ff7f000000000000000000000000000000000000000000000000000000000000000016602082015260208152613e21604082613d60565b815191929115615072576fffffffffffffffffffffffffffffffff6040840151166fffffffffffffffffffffffffffffffff6020850151161061500f576146cc91925b805182547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff0000000000000000000000000000000000000000161782556020810151825460409290920151608090811b7fffffffffffffffffffffffffffffffff00000000000000000000000000000000166fffffffffffffffffffffffffffffffff9290921691821760018501557fffffffffffffffffffffffff0000000000000000000000000000000000000000909216174290911b73ffffffff0000000000000000000000000000000016179055565b606483615070604051917f8020d12400000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565bfd5b6fffffffffffffffffffffffffffffffff60408401511615801590615101575b6150a0576146cc9192614f33565b606483615070604051917fd68af9cc00000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b506fffffffffffffffffffffffffffffffff6020840151161515615092565b906127109167ffffffffffffffff61513a60208301614592565b166000908152600b60205260409020917fffffffff00000000000000000000000000000000000000000000000000000000161561518b57606061ffff615187935460901c16910135613f0d565b0490565b606061ffff615187935460801c16910135613f0d565b91908201809211613f2057565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115615285577dffff0000000000000000000000000000000000000000000000000000000081161561527c5760ff60015b169060f01c80615246575b506001036152195750565b7fc512f96c0000000000000000000000000000000000000000000000000000000060005260045260246000fd5b60005b60108110615257575061520e565b6001811b821661526a575b600101615249565b9160018101809111613f205791615262565b60ff6000615203565b5050565b9167ffffffffffffffff7f50f6fbee3ceedce6b7fd7eaef18244487867e6718aec7208187efb6b7908c14c9216928360005260086020526152d28183600260406000200161585a565b6040805173ffffffffffffffffffffffffffffffffffffffff909216825260208201929092529081908101614d3f565b91909167ffffffffffffffff83169283600052600560205260ff60406000205460a01c16156153675750907fc6735cd4fa2bbe7b203b1682936e6ee61bc1702464bbbd12abb6630229d9a5f9918360005260056020526152d28183604060002061585a565b906146cc9350615289565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe182360301811215613af757016020813591019167ffffffffffffffff8211613af7578136038313613af757565b9167ffffffffffffffff7fff0133389f9bb82d5b9385826160eaf2328039f6fa950eeb8cf0836da81789449216928360005260086020526152d28183604060002061585a565b91909167ffffffffffffffff83169283600052600460205260ff60406000205460a01c161561546d5750907f28d6c52e2b0b7587b0d195539fbe6af984b28791aca4d2cc0844244e38bce29e918360005260046020526152d28183604060002061585a565b906146cc93506153c2565b906040519182815491828252602082019060005260206000209260005b8181106154aa5750506146cc92500383613d60565b8454835260019485019487945060209093019201615495565b80548210156146115760005260206000200190600090565b600081815260076020526040902054801561566a577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613f2057600654907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613f20578181036155fb575b50505060065480156155cc577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff016155898160066154c3565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b19169055600655600052600760205260006040812055600190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b61565261560c61561d9360066154c3565b90549060031b1c92839260066154c3565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b90556000526007602052604060002055388080615550565b5050600090565b906001820191816000528260205260406000205480151560001461579c577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613f20578254907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613f2057818103615765575b505050805480156155cc577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff019061572682826154c3565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b191690555560005260205260006040812055600190565b61578561577561561d93866154c3565b90549060031b1c928392866154c3565b9055600052836020526040600020553880806156ee565b50505050600090565b806000526007602052604060002054156000146157ff5760065468010000000000000000811015613cdd576157e661561d82600185940160065560066154c3565b9055600654906000526007602052604060002055600190565b50600090565b600082815260018201602052604090205461566a5780549068010000000000000000821015613cdd578261584361561d8460018096018555846154c3565b905580549260005201602052604060002055600190565b9182549060ff8260a01c16158015615b0e575b615b08576fffffffffffffffffffffffffffffffff821691600185019081546158b263ffffffff6fffffffffffffffffffffffffffffffff83169360801c1642613f88565b9081615a6a575b5050848110615a1e57508383106159135750506158e86fffffffffffffffffffffffffffffffff928392613f88565b16167fffffffffffffffffffffffffffffffff00000000000000000000000000000000825416179055565b9190915460801c9283156159b2578161592b91613f88565b927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810190808211613f205761597961597e9273ffffffffffffffffffffffffffffffffffffffff966151a1565b613f4f565b7fd0c8d23a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b73ffffffffffffffffffffffffffffffffffffffff83837fd0c8d23a000000000000000000000000000000000000000000000000000000006000527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6004526024521660445260646000fd5b828573ffffffffffffffffffffffffffffffffffffffff927f1a76572a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b828692939611615ade57615a8592614bc29160801c90613f0d565b80841015615ad95750825b85547fffffffffffffffffffffffff00000000ffffffffffffffffffffffffffffffff164260801b73ffffffff00000000000000000000000000000000161786559238806158b9565b615a90565b7f9725942a0000000000000000000000000000000000000000000000000000000060005260046000fd5b50505050565b50821561586d56fea164736f6c634300081a000a' as const +// generate:end diff --git a/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/cross-chain-token.ts b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/cross-chain-token.ts new file mode 100644 index 00000000..52ca7b62 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/cross-chain-token.ts @@ -0,0 +1,4 @@ +export default // generate: +// `'${require('fs').readFileSync(require.resolve('@chainlink/contracts-ccip/bytecode/v2_0_0/cross_chain_token.bin'), 'utf8').trim()}' as const` +'0x60c06040523461072757612e58803803806100198161072c565b92833981016060828203126107275781516001600160401b03811161072757820160e081830312610727576040519160e083016001600160401b0381118482101761061e5760405281516001600160401b038111610727578161007d918401610751565b83526020820151906001600160401b0382116107275761009e918301610751565b9081602084015260408101519060408401918252606081015191606085019283526100cb608083016107bc565b916080860192835260a08101519060ff821682036107275760c06100f69160a08901938452016107bc565b9460c08701958652610116604061010f60208b016107bc565b99016107bc565b6001600160a01b038116610721575033965b518051906001600160401b03821161061e5760035490600182811c92168015610717575b60208310146105fe5781601f8493116106a7575b50602090601f831160011461063f57600092610634575b50508160011b916000199060031b1c1916176003555b8051906001600160401b03821161061e57600454600181811c91168015610614575b60208210146105fe57601f8111610599575b50602090601f831160011461052d5760ff93929160009183610522575b50508160011b916000199060031b1c1916176004555b51166080525160a0528151156104f75780516001600160a01b0316156104e657519051906001600160a01b031680156104d0573081146104bc57600254918083018093116104a6576020926002557fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef600093849284845283825260408420818154019055604051908152a360a05180610480575b50505b516001600160a01b03168061047b5750335b600580546001600160a01b039283166001600160a01b0319821681179092559091167f9524c9e4b0b61eb018dd58a1cd856e3e74009528328ab4a613b434fa631d7242600080a36001600160a01b0381161561046557600780546001600160d01b0316905561030b906107d0565b506001600160a01b038116610455575b7f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6600081815260066020527f3195c024b2ddd6d9b8f6c836aa52f67fe69376c8903d009b80229b3ce4425f528054600080516020612e3883398151915291829055909290917fbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff9080a47f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a848600081815260066020527f42d20fd6db25ea5a8e33f43724ad72f2ebd9488257fa78c86176b8175fc383fb8054600080516020612e3883398151915291829055909290917fbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff9080a460405161257990816108bf823960805181611417015260a051818181610330015261113a0152f35b61045e9061081b565b503861031b565b636116401160e11b600052600060045260246000fd5b61029d565b6002548181116104905750610288565b637502c12360e11b835260045260245260449150fd5b634e487b7160e01b600052601160045260246000fd5b63ec442f0560e01b60005260045260246000fd5b63ec442f0560e01b600052600060045260246000fd5b634dd371db60e11b60005260046000fd5b516001600160a01b031690508061050e575061028b565b63f5c8f5a160e01b60005260045260246000fd5b0151905038806101de565b90601f198316916004600052816000209260005b818110610581575091600193918560ff97969410610568575b505050811b016004556101f4565b015160001960f88460031b161c1916905538808061055a565b92936020600181928786015181550195019301610541565b60046000527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b601f840160051c810191602085106105f4575b601f0160051c01905b8181106105e857506101c1565b600081556001016105db565b90915081906105d2565b634e487b7160e01b600052602260045260246000fd5b90607f16906101af565b634e487b7160e01b600052604160045260246000fd5b015190503880610177565b600360009081528281209350601f198516905b81811061068f5750908460019594939210610676575b505050811b0160035561018d565b015160001960f88460031b161c19169055388080610668565b92936020600181928786015181550195019301610652565b60036000529091507fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f840160051c8101916020851061070d575b90601f859493920160051c01905b8181106106fe5750610160565b600081558493506001016106f1565b90915081906106e3565b91607f169161014c565b96610128565b600080fd5b6040519190601f01601f191682016001600160401b0381118382101761061e57604052565b81601f82011215610727578051906001600160401b03821161061e57610780601f8301601f191660200161072c565b92828452602083830101116107275760005b8281106107a757505060206000918301015290565b80602080928401015182828701015201610792565b51906001600160a01b038216820361072757565b600854906001600160a01b03821661080a576001600160a01b03199091166001600160a01b0382161760085561080790600061082f565b90565b631fe1e13d60e11b60005260046000fd5b61080790600080516020612e388339815191525b60008181526006602090815260408083206001600160a01b038616845290915290205460ff166108b75760008181526006602090815260408083206001600160a01b0395909516808452949091528120805460ff19166001179055339291907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d9080a4600190565b505060009056fe608080604052600436101561001357600080fd5b60003560e01c90816301ffc9a7146119d457508063022d63fb1461199857806306fdde03146118bb578063095ea7b3146117795780630aa6220b1461169357806318160ddd14611657578063181f5a77146115a157806323b872dd1461154b578063248a9ca3146114f8578063282c51f31461149f5780632f2ff15d1461143b578063313ce567146113df57806336568abe1461125057806340c10f191461105157806342966c681461100e578063634e93da14610eb7578063649a5ec714610c8757806370a0823114610c2257806379cc67901461095657806384ef8ffc14610bd05780638da5cb5b14610bd05780638fd6a6ac14610b7e57806391d1485414610b0557806395d89b41146109ac5780639dc29fac14610956578063a1eda53c146108d1578063a217fddf14610897578063a8fa343c146107ec578063a9059cbb1461079d578063c630948d146106ac578063c91ddc2014610653578063cc8463c81461060a578063cefc1429146104cc578063cf6eefb714610441578063d5391393146103e8578063d547741f14610353578063d5abeb01146102fa578063d602b9fd146102615763dd62ed3e146101cc57600080fd5b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57610203611c1c565b73ffffffffffffffffffffffffffffffffffffffff610220611c3f565b9116600052600160205273ffffffffffffffffffffffffffffffffffffffff604060002091166000526020526020604060002054604051908152f35b600080fd5b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57610298611cdc565b600780547fffffffffffff0000000000000000000000000000000000000000000000000000811690915560a01c65ffffffffffff166102d357005b7f8886ebfc4259abdbc16601dd8fb5678e54878f47b3c34836cfc51154a9605109600080a1005b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760206040517f00000000000000000000000000000000000000000000000000000000000000008152f35b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760043561038d611c3f565b81156103be57816103b76103b26103bc94600052600660205260016040600020015490565b611dd3565b6122f9565b005b7f3fc3c27a0000000000000000000000000000000000000000000000000000000060005260046000fd5b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760206040517f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a68152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57604065ffffffffffff6104a66007549065ffffffffffff73ffffffffffffffffffffffffffffffffffffffff83169260a01c1690565b73ffffffffffffffffffffffffffffffffffffffff849392935193168352166020820152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760075473ffffffffffffffffffffffffffffffffffffffff1633036105dc5760075460a081901c65ffffffffffff169073ffffffffffffffffffffffffffffffffffffffff16811580156105d2575b6105a4576105799061057373ffffffffffffffffffffffffffffffffffffffff6008541661228b565b506121af565b50600780547fffffffffffff0000000000000000000000000000000000000000000000000000169055005b507f19ca5ebb0000000000000000000000000000000000000000000000000000000060005260045260246000fd5b504282101561054a565b7fc22c8022000000000000000000000000000000000000000000000000000000006000523360045260246000fd5b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576020610643611ca3565b65ffffffffffff60405191168152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760206040517fcfd2b420c3d2b6ebd6af82f6e29c095b45a072b8d1b5d9eda2a56dcb850acaa68152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576103bc6106e6611c1c565b7f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a660005260066020527f3195c024b2ddd6d9b8f6c836aa52f67fe69376c8903d009b80229b3ce4425f525461073a90611dd3565b61074381612158565b507f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a84860005260066020527f42d20fd6db25ea5a8e33f43724ad72f2ebd9488257fa78c86176b8175fc383fb5461079890611dd3565b612185565b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576107e16107d7611c1c565b6024359033611f5d565b602060405160018152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57610823611c1c565b61082b611cdc565b73ffffffffffffffffffffffffffffffffffffffff80600554921691827fffffffffffffffffffffffff0000000000000000000000000000000000000000821617600555167f9524c9e4b0b61eb018dd58a1cd856e3e74009528328ab4a613b434fa631d7242600080a3005b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57602060405160008152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576008548060d01c908115158061094c575b156109425760a01c65ffffffffffff165b6040805165ffffffffffff928316815292909116602083015290f35b0390f35b5050600080610922565b5042821015610911565b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576103bc610990611c1c565b6024359061099c611d48565b6109a7823383611e40565b61208d565b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760405160006004548060011c90600181168015610afb575b602083108114610ace57828552908115610a8c5750600114610a2c575b61093e83610a2081850382611c62565b60405191829182611bb4565b91905060046000527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b916000905b808210610a7257509091508101602001610a20610a10565b919260018160209254838588010152019101909291610a5a565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660208086019190915291151560051b84019091019150610a209050610a10565b6024847f4e487b710000000000000000000000000000000000000000000000000000000081526022600452fd5b91607f16916109f3565b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57610b3c611c3f565b600435600052600660205273ffffffffffffffffffffffffffffffffffffffff60406000209116600052602052602060ff604060002054166040519015158152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57602073ffffffffffffffffffffffffffffffffffffffff60055416604051908152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57602073ffffffffffffffffffffffffffffffffffffffff60085416604051908152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5773ffffffffffffffffffffffffffffffffffffffff610c6e611c1c565b1660005260006020526020604060002054604051908152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760043565ffffffffffff81169081810361025c57610cd2611cdc565b610cdb4261236f565b9165ffffffffffff610ceb611ca3565b1680821115610e4e57507ff1038c18cf84a56e432fdbfaf746924b7ea511dfe03a6506a0ceba4888788d9b9265ffffffffffff826206978080610d3895109118026206978018169061213a565b906008548060d01c80610dca575b50506008805473ffffffffffffffffffffffffffffffffffffffff1660a083901b79ffffffffffff0000000000000000000000000000000000000000161760d084901b7fffffffffffff0000000000000000000000000000000000000000000000000000161790556040805165ffffffffffff9283168152919092166020820152a1005b421115610e235779ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff00000000000000000000000000000000000000000000000000006007549260301b169116176007555b8380610d46565b507f2b1fa2edafe6f7b9e97c1a9e0c3660e645beb2dcaa2d45bdbf9beaf5472e1ec5600080a1610e1c565b0365ffffffffffff8111610e88577ff1038c18cf84a56e432fdbfaf746924b7ea511dfe03a6506a0ceba4888788d9b92610d38919061213a565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57610eee611c1c565b610ef6611cdc565b7f3377dc44241e779dd06afab5b788a35ca5f3b778836e2990bdb26a2a4b2e5ed66020610f33610f254261236f565b610f2d611ca3565b9061213a565b65ffffffffffff73ffffffffffffffffffffffffffffffffffffffff610f7c6007549065ffffffffffff73ffffffffffffffffffffffffffffffffffffffff83169260a01c1690565b9690501694600754867fffffffffffff000000000000000000000000000000000000000000000000000079ffffffffffff00000000000000000000000000000000000000008660a01b169216171760075516610fe4575b65ffffffffffff60405191168152a2005b7f8886ebfc4259abdbc16601dd8fb5678e54878f47b3c34836cfc51154a9605109600080a1610fd3565b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57611045611d48565b6103bc6004353361208d565b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57611088611c1c565b3360009081527f3195c024b2ddd6d9b8f6c836aa52f67fe69376c8903d009b80229b3ce4425f516020526040902054602435919060ff16156111fe5773ffffffffffffffffffffffffffffffffffffffff1680156111cf573081146111a25760025491808301809311610e88576020926002557fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef600093849284845283825260408420818154019055604051908152a37f000000000000000000000000000000000000000000000000000000000000000080611162575080f35b90600254918083116111745750905080f35b6044927fea058246000000000000000000000000000000000000000000000000000000008352600452602452fd5b7fec442f050000000000000000000000000000000000000000000000000000000060005260045260246000fd5b7fec442f0500000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b7fe2517d3f00000000000000000000000000000000000000000000000000000000600052336004527f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a660245260446000fd5b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760043561128a611c3f565b8115806113a8575b6112e7575b3373ffffffffffffffffffffffffffffffffffffffff8216036112bd576103bc916122f9565b7f6697b2320000000000000000000000000000000000000000000000000000000060005260046000fd5b60075465ffffffffffff60a082901c169073ffffffffffffffffffffffffffffffffffffffff1615801590611398575b8015611386575b61135057507fffffffffffff000000000000ffffffffffffffffffffffffffffffffffffffff60075416600755611297565b65ffffffffffff907f19ca5ebb000000000000000000000000000000000000000000000000000000006000521660045260246000fd5b504265ffffffffffff8216101561131e565b5065ffffffffffff811615611317565b5073ffffffffffffffffffffffffffffffffffffffff6008541673ffffffffffffffffffffffffffffffffffffffff821614611292565b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57600435611475611c3f565b81156103be578161149a6103b26103bc94600052600660205260016040600020015490565b612217565b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760206040517f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a8488152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576020611543600435600052600660205260016040600020015490565b604051908152f35b3461025c5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576107e1611585611c1c565b61158d611c3f565b6044359161159c833383611e40565b611f5d565b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57604051604081019080821067ffffffffffffffff8311176116285761093e91604052601581527f43726f7373436861696e546f6b656e20322e302e300000000000000000000000602082015260405191829182611bb4565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576020600254604051908152f35b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576116ca611cdc565b6008548060d01c806116f5575b6008805473ffffffffffffffffffffffffffffffffffffffff169055005b42111561174e5779ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff00000000000000000000000000000000000000000000000000006007549260301b169116176007555b80806116d7565b507f2b1fa2edafe6f7b9e97c1a9e0c3660e645beb2dcaa2d45bdbf9beaf5472e1ec5600080a1611747565b3461025c5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576117b0611c1c565b73ffffffffffffffffffffffffffffffffffffffff1660243530821461188d57331561185e57811561182f57336000526001602052604060002082600052602052806040600020556040519081527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b7f94280d6200000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b7fe602df0500000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b507f94280d620000000000000000000000000000000000000000000000000000000060005260045260246000fd5b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c5760405160006003548060011c9060018116801561198e575b602083108114610ace57828552908115610a8c575060011461192e5761093e83610a2081850382611c62565b91905060036000527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b916000905b80821061197457509091508101602001610a20610a10565b91926001816020925483858801015201910190929161195c565b91607f1691611902565b3461025c5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c576020604051620697808152f35b3461025c5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261025c57600435907fffffffff00000000000000000000000000000000000000000000000000000000821680920361025c57817f314987860000000000000000000000000000000000000000000000000000000060209314908115611b59575b8115611a9e575b8115611a74575b5015158152f35b7fe6599b4d0000000000000000000000000000000000000000000000000000000091501483611a6d565b90507f36372b070000000000000000000000000000000000000000000000000000000081148015611b30575b8015611b07575b8015611ade575b90611a66565b507f01ffc9a7000000000000000000000000000000000000000000000000000000008114611ad8565b507fa219a025000000000000000000000000000000000000000000000000000000008114611ad1565b507f8fd6a6ac000000000000000000000000000000000000000000000000000000008114611aca565b90507f7965db0b0000000000000000000000000000000000000000000000000000000081148015611b8b575b90611a5f565b507f01ffc9a7000000000000000000000000000000000000000000000000000000008114611b85565b9190916020815282519283602083015260005b848110611c065750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8460006040809697860101520116010190565b8060208092840101516040828601015201611bc7565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361025c57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361025c57565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff82111761162857604052565b6008548060d01c8015159081611cd2575b5015611cc85760a01c65ffffffffffff1690565b5060075460d01c90565b9050421138611cb4565b3360009081527f54cdd369e4e8a8515e52ca72ec816c2101831ad1f18bf44102ed171459c9b4f8602052604090205460ff1615611d1557565b7fe2517d3f0000000000000000000000000000000000000000000000000000000060005233600452600060245260446000fd5b3360009081527f42d20fd6db25ea5a8e33f43724ad72f2ebd9488257fa78c86176b8175fc383fa602052604090205460ff1615611d8157565b7fe2517d3f00000000000000000000000000000000000000000000000000000000600052336004527f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a84860245260446000fd5b806000526006602052604060002073ffffffffffffffffffffffffffffffffffffffff331660005260205260ff6040600020541615611e0f5750565b7fe2517d3f000000000000000000000000000000000000000000000000000000006000523360045260245260446000fd5b73ffffffffffffffffffffffffffffffffffffffff9092919216806000526001602052604060002073ffffffffffffffffffffffffffffffffffffffff8416600052602052604060002054927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8410611eba575b50505050565b828410611f115773ffffffffffffffffffffffffffffffffffffffff169030821461188d57801561185e57811561182f57600052600160205260406000209060005260205260406000209103905538808080611eb4565b8373ffffffffffffffffffffffffffffffffffffffff84927ffb8f41b2000000000000000000000000000000000000000000000000000000006000521660045260245260445260646000fd5b73ffffffffffffffffffffffffffffffffffffffff1690811561205e5773ffffffffffffffffffffffffffffffffffffffff169182156111cf57308314612030576000828152806020526040812054828110611ffd5791604082827fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef958760209652828652038282205586815280845220818154019055604051908152a3565b6064937fe450d38c0000000000000000000000000000000000000000000000000000000083949352600452602452604452fd5b827fec442f050000000000000000000000000000000000000000000000000000000060005260045260246000fd5b7f96c6fd1e00000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b73ffffffffffffffffffffffffffffffffffffffff16801561205e5730156111cf5760009181835282602052604083205481811061210857817fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef926020928587528684520360408620558060025403600255604051908152a3565b83927fe450d38c0000000000000000000000000000000000000000000000000000000060649552600452602452604452fd5b9065ffffffffffff8091169116019065ffffffffffff8211610e8857565b612182907f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a66123b9565b90565b612182907f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a8486123b9565b6008549073ffffffffffffffffffffffffffffffffffffffff82166103be57612182917fffffffffffffffffffffffff000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff831691161760085560006123b9565b908115612228575b612182916123b9565b6008549173ffffffffffffffffffffffffffffffffffffffff83166103be577fffffffffffffffffffffffff000000000000000000000000000000000000000090921673ffffffffffffffffffffffffffffffffffffffff82161760085561221f565b6121829073ffffffffffffffffffffffffffffffffffffffff6008541673ffffffffffffffffffffffffffffffffffffffff8216146122cc575b6000612498565b7fffffffffffffffffffffffff0000000000000000000000000000000000000000600854166008556122c5565b9061218291801580612338575b15612498577fffffffffffffffffffffffff000000000000000000000000000000000000000060085416600855612498565b5073ffffffffffffffffffffffffffffffffffffffff6008541673ffffffffffffffffffffffffffffffffffffffff831614612306565b65ffffffffffff81116123875765ffffffffffff1690565b7f6dfcc65000000000000000000000000000000000000000000000000000000000600052603060045260245260446000fd5b806000526006602052604060002073ffffffffffffffffffffffffffffffffffffffff831660005260205260ff604060002054161560001461249157806000526006602052604060002073ffffffffffffffffffffffffffffffffffffffff8316600052602052604060002060017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0082541617905573ffffffffffffffffffffffffffffffffffffffff339216907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d600080a4600190565b5050600090565b806000526006602052604060002073ffffffffffffffffffffffffffffffffffffffff831660005260205260ff6040600020541660001461249157806000526006602052604060002073ffffffffffffffffffffffffffffffffffffffff831660005260205260406000207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00815416905573ffffffffffffffffffffffffffffffffffffffff339216907ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b600080a460019056fea164736f6c634300081a000acfd2b420c3d2b6ebd6af82f6e29c095b45a072b8d1b5d9eda2a56dcb850acaa6' as const +// generate:end diff --git a/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/erc20-lockbox.ts b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/erc20-lockbox.ts new file mode 100644 index 00000000..e69f8fd5 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/erc20-lockbox.ts @@ -0,0 +1,4 @@ +export default // generate: +// `'${require('fs').readFileSync(require.resolve('@chainlink/contracts-ccip/bytecode/v2_0_0/erc20_lock_box.bin'), 'utf8').trim()}' as const` +'0x60a0604052346101d9576113cf6020813803918261001c816101de565b9384928339810103126101d957516001600160a01b038116908190036101d957602090610048826101de565b9160008352600036813733156101c857600180546001600160a01b03191633179055610073816101de565b60008152600036813760408051949085016001600160401b038111868210176101b2576040528452808285015260005b815181101561010a576001906001600160a01b036100c18285610203565b5116846100cd82610245565b6100da575b5050016100a3565b7fc3803387881faad271c47728894e3e36fac830ffc8602ca6fc07733cbda7758091604051908152a138846100d2565b5050915160005b8151811015610182576001600160a01b0361012c8284610203565b5116908115610171577feb1b9b92e50b7f88f9ff25d56765095ac6e91540eee214906f4036a908ffbdef8583610163600195610343565b50604051908152a101610111565b6342bcdf7f60e11b60005260046000fd5b8280156101715760805260405161102b90816103a482396080518181816105f6015281816109960152610c060152f35b634e487b7160e01b600052604160045260246000fd5b639b15e16f60e01b60005260046000fd5b600080fd5b6040519190601f01601f191682016001600160401b038111838210176101b257604052565b80518210156102175760209160051b010190565b634e487b7160e01b600052603260045260246000fd5b80548210156102175760005260206000200190600090565b600081815260036020526040902054801561033c57600019810181811161032657600254600019810191908211610326578082036102d5575b50505060025480156102bf576000190161029981600261022d565b8154906000199060031b1b19169055600255600052600360205260006040812055600190565b634e487b7160e01b600052603160045260246000fd5b61030e6102e66102f793600261022d565b90549060031b1c928392600261022d565b819391549060031b91821b91600019901b19161790565b9055600052600360205260406000205538808061027e565b634e487b7160e01b600052601160045260246000fd5b5050600090565b8060005260036020526040600020541560001461039d57600254680100000000000000008110156101b2576103846102f7826001859401600255600261022d565b9055600254906000526003602052604060002055600190565b5060009056fe608080604052600436101561001357600080fd5b60003560e01c908163181f5a77146109ba5750806321df0da71461094b5780632451a6271461085d57806374fd18ac1461061b57806375151b631461058c57806379ba5097146104a35780638da5cb5b1461045157806391a2749a14610267578063a36a7fee146101825763f2fde38b1461008d57600080fd5b3461017d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d5773ffffffffffffffffffffffffffffffffffffffff6100d9610a89565b6100e1610cc8565b1633811461015357807fffffffffffffffffffffffff0000000000000000000000000000000000000000600054161760005573ffffffffffffffffffffffffffffffffffffffff600154167fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae1278600080a3005b7fdad89dca0000000000000000000000000000000000000000000000000000000060005260046000fd5b600080fd5b3461017d5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d576101b9610a89565b6101c1610aac565b5073ffffffffffffffffffffffffffffffffffffffff604435916101e58382610bd3565b166102396040517f23b872dd00000000000000000000000000000000000000000000000000000000602082015233602482015230604482015283606482015260648152610233608482610b0e565b82610d56565b6040519182527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203393a3005b3461017d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d5760043567ffffffffffffffff811161017d5760407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc823603011261017d57604051906102e182610ac3565b806004013567ffffffffffffffff811161017d576103059060043691840101610b4f565b825260248101359067ffffffffffffffff821161017d57600461032b9236920101610b4f565b6020820190815261033a610cc8565b519060005b82518110156103b2578073ffffffffffffffffffffffffffffffffffffffff61036a60019386610d13565b511661037581610df9565b610381575b500161033f565b60207fc3803387881faad271c47728894e3e36fac830ffc8602ca6fc07733cbda7758091604051908152a18461037a565b505160005b815181101561044f5773ffffffffffffffffffffffffffffffffffffffff6103df8284610d13565b5116908115610425577feb1b9b92e50b7f88f9ff25d56765095ac6e91540eee214906f4036a908ffbdef602083610417600195610fbe565b50604051908152a1016103b7565b7f8579befe0000000000000000000000000000000000000000000000000000000060005260046000fd5b005b3461017d5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d57602073ffffffffffffffffffffffffffffffffffffffff60015416604051908152f35b3461017d5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d5760005473ffffffffffffffffffffffffffffffffffffffff81163303610562577fffffffffffffffffffffffff00000000000000000000000000000000000000006001549133828416176001551660005573ffffffffffffffffffffffffffffffffffffffff3391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0600080a3005b7f02b543c60000000000000000000000000000000000000000000000000000000060005260046000fd5b3461017d5760207ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d5760206105c5610a89565b73ffffffffffffffffffffffffffffffffffffffff604051911673ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016148152f35b3461017d5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d57610652610a89565b61065a610aac565b506044356064359173ffffffffffffffffffffffffffffffffffffffff831680930361017d57819061068c8382610bd3565b83156108335773ffffffffffffffffffffffffffffffffffffffff1691604051907f70a08231000000000000000000000000000000000000000000000000000000008252306004830152602082602481875afa918215610827576000926107d0575b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146107c8575b808211610797575060207f2717ead6b9200dd235aad468c9809ea400fe33ac69b5bfaa6d3e90fc922b63989161078e6040517fa9059cbb000000000000000000000000000000000000000000000000000000008482015286602482015282604482015260448152610788606482610b0e565b85610d56565b604051908152a3005b907fcf4791810000000000000000000000000000000000000000000000000000000060005260045260245260446000fd5b905080610716565b90916020823d60201161081f575b816107eb60209383610b0e565b8101031261081c575051907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6106ee565b80fd5b3d91506107de565b6040513d6000823e3d90fd5b7fd87070520000000000000000000000000000000000000000000000000000000060005260046000fd5b3461017d5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d576040518060206002549283815201809260026000527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace9060005b81811061093557505050816108dc910382610b0e565b6040519182916020830190602084525180915260408301919060005b818110610906575050500390f35b825173ffffffffffffffffffffffffffffffffffffffff168452859450602093840193909201916001016108f8565b82548452602090930192600192830192016108c6565b3461017d5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d57602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b3461017d5760007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261017d576109f281610ac3565b601281527f45524332304c6f636b426f7820322e302e300000000000000000000000000000602082015260405190602082528181519182602083015260005b838110610a715750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f836000604080968601015201168101030190f35b60208282018101516040878401015285935001610a31565b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361017d57565b6024359067ffffffffffffffff8216820361017d57565b6040810190811067ffffffffffffffff821117610adf57604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117610adf57604052565b81601f8201121561017d5780359167ffffffffffffffff8311610adf578260051b9160405193610b826020850186610b0e565b845260208085019382010191821161017d57602001915b818310610ba65750505090565b823573ffffffffffffffffffffffffffffffffffffffff8116810361017d57815260209283019201610b99565b9015610c9e5773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168103610c71575033600052600360205260406000205415610c4357565b7fd86ad9cf000000000000000000000000000000000000000000000000000000006000523360045260246000fd5b7fbf16aab60000000000000000000000000000000000000000000000000000000060005260045260246000fd5b7f8b1fa9dd0000000000000000000000000000000000000000000000000000000060005260046000fd5b73ffffffffffffffffffffffffffffffffffffffff600154163303610ce957565b7f2b5c74de0000000000000000000000000000000000000000000000000000000060005260046000fd5b8051821015610d275760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b906000602091828151910182855af115610827576000513d610dd8575073ffffffffffffffffffffffffffffffffffffffff81163b155b610d945750565b73ffffffffffffffffffffffffffffffffffffffff907f5274afe7000000000000000000000000000000000000000000000000000000006000521660045260246000fd5b60011415610d8d565b8054821015610d275760005260206000200190600090565b6000818152600360205260409020548015610fb7577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111610f8857600254907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211610f8857808203610f19575b5050506002548015610eea577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01610ea7816002610de1565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b19169055600255600052600360205260006040812055600190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b610f70610f2a610f3b936002610de1565b90549060031b1c9283926002610de1565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b90556000526003602052604060002055388080610e6e565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b5050600090565b806000526003602052604060002054156000146110185760025468010000000000000000811015610adf57610fff610f3b8260018594016002556002610de1565b9055600254906000526003602052604060002055600190565b5060009056fea164736f6c634300081a000a' as const +// generate:end diff --git a/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/lock-release-token-pool.ts b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/lock-release-token-pool.ts new file mode 100644 index 00000000..fbc5dcd8 --- /dev/null +++ b/ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/lock-release-token-pool.ts @@ -0,0 +1,4 @@ +export default // generate: +// `'${require('fs').readFileSync(require.resolve('@chainlink/contracts-ccip/bytecode/v2_0_0/lock_release_token_pool.bin'), 'utf8').trim()}' as const` +'0x610100806040523461037a5760c081616038803803809161002082856103ba565b83398101031261037a5780516001600160a01b0381169182820361037a5761004a602082016103f3565b9061005760408201610401565b61006360608301610401565b9261007c60a061007560808601610401565b9401610401565b9333156103a957600180546001600160a01b0319163317905586158015610398575b8015610387575b6102df578560805260c0523086036102f0575b60a052600380546001600160a01b03199081166001600160a01b03938416179091556002805490911692821692909217909155169182156102df576040516375151b6360e01b815260048101829052602081602481875afa9081156102d357600091610291575b501561027d57604051906020600081840163095ea7b360e01b815286602486015281196044860152604485526101566064866103ba565b84519082875af1903d600051908361025e575b50505015610219575b8260e052604051615bc79081610471823960805181818161024a015281816121d3015281816129c701528181612c3a015281816130e8015281816137140152818161376e0152614f0c015260a0518181816135da015281816148ed015281816149370152614f60015260c0518181816102e50152818161134d0152818161226d01528181612a620152613183015260e0518181816126cf01528181612bc10152614e910152f35b6102579161025260405163095ea7b360e01b6020820152856024820152600060448201526044815261024c6064826103ba565b82610415565b610415565b3880610172565b9192509061027357503b15155b388080610169565b600191501461026b565b63961c9a4f60e01b60005260045260246000fd5b6020813d6020116102cb575b816102aa602093836103ba565b810103126102c757519081151582036102c457503861011f565b80fd5b5080fd5b3d915061029d565b6040513d6000823e3d90fd5b630a64406560e11b60005260046000fd5b60405163313ce56760e01b81526020816004818a5afa60009181610346575b5061031b575b506100b8565b60ff1660ff821681810361032f5750610315565b6332ad3e0760e11b60005260045260245260446000fd5b9091506020813d60201161037f575b81610362602093836103ba565b8101031261037a57610373906103f3565b903861030f565b600080fd5b3d9150610355565b506001600160a01b038116156100a5565b506001600160a01b0384161561009e565b639b15e16f60e01b60005260046000fd5b601f909101601f19168101906001600160401b038211908210176103dd57604052565b634e487b7160e01b600052604160045260246000fd5b519060ff8216820361037a57565b51906001600160a01b038216820361037a57565b906000602091828151910182855af1156102d3576000513d61046757506001600160a01b0381163b155b6104465750565b635274afe760e01b60009081526001600160a01b0391909116600452602490fd5b6001141561043f56fe608080604052600436101561001357600080fd5b600090813560e01c90816301ffc9a71461398f5750806306b859ef146138aa578063181f5a77146138495780631826b1e71461379257806321df0da714613741578063240028e8146136dd5780632422ac45146135fe57806324f65ee7146135c05780632cab0fb61461304d57806337a3210d14613019578063390775371461291c5780634c5ef0ed146128d557806362ddd3c41461284e5780637437ff9f1461280057806379ba5097146127395780638926f54f146126f35780638c6894fb146126a25780638da5cb5b1461266e5780639a4575b91461215a578063a42a7b8b14611ff3578063acfecf9114611efb578063ae39a25714611d70578063b6cfa3b714611cb5578063b794658014611c7d578063bfeffd3f14611bd1578063c4bffe2b14611aa6578063c7230a60146117f5578063dc04fa1f14611371578063dc0bd97114611320578063dcbd41bc1461111c578063e8a1da1714610a44578063ea6396db14610906578063ec6ae7a7146108c3578063f2fde38b146107f45763fbc801a7146101a257600080fd5b34610668576060600319360112610668576004359067ffffffffffffffff8211610668578160040160a060031984360301126107f0576101e0613ac1565b9160443567ffffffffffffffff81116107f0579061020661022393923690600401613bec565b93906102106145a9565b5061021b86856151c4565b943691613d66565b93608486019461023286614536565b73ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116036107a657602487019677ffffffffffffffff0000000000000000000000000000000061029889614557565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115610719578591610777575b5061074f5767ffffffffffffffff61032c89614557565b16610344816000526007602052604060002054151590565b1561072457602073ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156107195785906106c8575b73ffffffffffffffffffffffffffffffffffffffff915016330361069c576064810135946103d38787613f4d565b7fffffffff00000000000000000000000000000000000000000000000000000000851694851561067a5761042f907fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1690614a41565b61044b8161043c8b614536565b6104458d614557565b906154ac565b73ffffffffffffffffffffffffffffffffffffffff600354169384610549575b61053f8a61050e6105098e6104808e8e613f4d565b936104938561048e84614557565b614e7a565b7ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff6104cf6104c985614557565b93614536565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252336020830152810188905292169180606081015b0390a2614557565b61471a565b90610517614f59565b6040519261052484613cd1565b83526020830152604051928392604084526040840190613e2e565b9060208301520390f35b843b15610676578694928a949286928d604051998a98899788967fa8027c0f00000000000000000000000000000000000000000000000000000000885260048801608090528061059891615416565b6084890160a090526101248901906105af92613f7b565b936105b990613bd7565b67ffffffffffffffff1660a48801526044016105d490613b88565b73ffffffffffffffffffffffffffffffffffffffff1660c48701528d60e48701526105fe90613b88565b73ffffffffffffffffffffffffffffffffffffffff16610104860152602485015283810360031901604485015261063491613c1a565b90606483015203925af1801561066b57610653575b808080808061046b565b61065e828092613d25565b6106685780610649565b80fd5b6040513d84823e3d90fd5b8680fd5b50610697816106888b614536565b6106918d614557565b90615466565b61044b565b6024847f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b506020813d602011610711575b816106e260209383613d25565b8101031261070d5761070873ffffffffffffffffffffffffffffffffffffffff91613f5a565b6103a5565b8480fd5b3d91506106d5565b6040513d87823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008552600452602484fd5b6004847f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b610799915060203d60201161079f575b6107918183613d25565b810190614bad565b38610315565b503d610787565b60248373ffffffffffffffffffffffffffffffffffffffff6107c789614536565b7f961c9a4f00000000000000000000000000000000000000000000000000000000835216600452fd5b5080fd5b50346106685760206003193601126106685773ffffffffffffffffffffffffffffffffffffffff610823613b1f565b61082b614bc5565b1633811461089b57807fffffffffffffffffffffffff000000000000000000000000000000000000000083541617825573ffffffffffffffffffffffffffffffffffffffff600154167fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12788380a380f35b6004827fdad89dca000000000000000000000000000000000000000000000000000000008152fd5b503461066857806003193601126106685760207fffffffff0000000000000000000000000000000000000000000000000000000060025460401b16604051908152f35b503461066857608060031936011261066857610920613b1f565b50610929613ba9565b610931613af0565b5060643567ffffffffffffffff8111610a40579167ffffffffffffffff60409261096160e0953690600401613bec565b50508260c0855161097181613d09565b82815282602082015282878201528260608201528260808201528260a08201520152168152600b60205220604051906109a982613d09565b5461ffff818163ffffffff82169485815263ffffffff60208201818560201c1681528160408401818760401c168152816060860193818960601c16855260ff60c060808901988a8c60801c168a528a60a082019c60901c168c52019b60a01c1615158b526040519b8c52511660208b0152511660408901525116606087015251166080850152511660a083015251151560c0820152f35b8280fd5b50346106685760406003193601126106685760043567ffffffffffffffff81116107f057610a76903690600401613e58565b9060243567ffffffffffffffff81116111185790610a9984923690600401613e58565b939091610aa4614bc5565b83905b828210610f595750505081927ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee182360301935b81811015610f55578060051b8301358581121561070d5783016101208136031261070d5760405194610b0b86613ced565b610b1482613bd7565b8652602082013567ffffffffffffffff81116107f05782019436601f870112156107f057853595610b4487613eba565b96610b526040519889613d25565b80885260208089019160051b8301019036821161070d5760208301905b828210610f26575050505060208701958652604083013567ffffffffffffffff8111610a4057610ba29036908501613dcb565b9160408801928352610bcc610bba36606087016147c6565b9460608a0195865260c03691016147c6565b956080890196875283515115610efe57610bf067ffffffffffffffff8a5116615849565b15610ec75767ffffffffffffffff8951168252600860205260408220610c17865182614f94565b610c25885160028301614f94565b6004855191019080519067ffffffffffffffff8211610e9a57610c488354614605565b601f8111610e5f575b50602090601f8311600114610dc057610c9f9291869183610db5575b50507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b90555b815b88518051821015610cd95790610cd3600192610ccc8367ffffffffffffffff8f5116926145c2565b5190614c10565b01610ca4565b5050977f8d340f17e19058004c20453540862a9c62778504476f6756755cb33bcd6c38c2939199975095610da767ffffffffffffffff6001979694985116925193519151610d73610d3e60405196879687526101006020880152610100870190613c1a565b9360408601906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b60a08401906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b0390a1019392909193610ada565b015190508e80610c6d565b83865281862091907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08416875b818110610e475750908460019594939210610e10575b505050811b019055610ca2565b01517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690558d8080610e03565b92936020600181928786015181550195019301610ded565b610e8a9084875260208720601f850160051c81019160208610610e90575b601f0160051c0190614862565b8d610c51565b9091508190610e7d565b6024857f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b60248267ffffffffffffffff8b51167f1d5ad3c5000000000000000000000000000000000000000000000000000000008252600452fd5b6004827f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b813567ffffffffffffffff811161067657602091610f4a8392833691890101613dcb565b815201910190610b6f565b8380f35b9267ffffffffffffffff610f7b610f768486889a9699979a614799565b614557565b1691610f868361557f565b156110ec578284526008602052610fa26005604086200161551c565b94845b8651811015610fdb576001908587526008602052610fd460056040892001610fcd838b6145c2565b5190615715565b5001610fa5565b50939692909450949094808752600860205260056040882088815588600182015588600282015588600382015588600482016110178154614605565b806110ab575b505050018054908881558161108d575b5050907f5204aec90a3c794d8e90fded8b46ae9c7c552803e7e832e0c1d358396d8599166020836001948a52600482528985604082208281550155808a52600582528985604082208281550155604051908152a101909194939294610aa7565b885260208820908101905b8181101561102d57888155600101611098565b601f81116001146110c15750555b888a8061101d565b818352602083206110dc91601f01861c810190600101614862565b80825281602081209155556110b9565b602484847f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b8380fd5b50346106685760206003193601126106685760043567ffffffffffffffff81116107f05761114e903690600401613e89565b73ffffffffffffffffffffffffffffffffffffffff600a5416331415806112fe575b6112d257825b818110611181578380f35b61118c81838561473c565b67ffffffffffffffff61119e82614557565b16906111b7826000526007602052604060002054151590565b156112a657907f41f7c8f7cfdad9350aa495e6c54cbbf750a07ab38a9098aed1256e30dd1682bb60e083611266611240602060019897018b6111f88261474c565b1561126d57879052600460205261121f60408d2061121936604088016147c6565b90614f94565b868c52600560205261123b60408d206112193660a088016147c6565b61474c565b916040519215158352611259602084016040830161481e565b60a060808401910161481e565ba201611176565b60026040828a61123b9452600860205261128f82822061121936858c016147c6565b8a8152600860205220016112193660a088016147c6565b602486837f1e670e4b000000000000000000000000000000000000000000000000000000008252600452fd5b6024837f8e4a23d600000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff60015416331415611170565b5034610668578060031936011261066857602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346106685760406003193601126106685760043567ffffffffffffffff81116107f0576113a3903690600401613e89565b60243567ffffffffffffffff8111611118576113c3903690600401613e58565b9190926113ce614bc5565b845b82811061143a57505050825b8181106113e7578380f35b8067ffffffffffffffff611401610f766001948688614799565b16808652600b6020528560408120557f5479bbc0288b7eaeaf2ace0943b88016cc648964fcd42919a86fd93b15fdbee88680a2016113dc565b67ffffffffffffffff611451610f7683868661473c565b16611469816000526007602052604060002054151590565b156117ca5761147982858561473c565b602081019060e081019061148c8261474c565b1561179e5760a0810161271061ffff6114a483614759565b16101561178f5760c082019161271061ffff6114bf85614759565b1610156117575763ffffffff6114d486614768565b161561172b57858c52600b60205260408c206114ef86614768565b63ffffffff1690805490604084019161150783614768565b60201b67ffffffff000000001693606086019461152386614768565b60401b6bffffffff000000000000000016966080019661154288614768565b60601b6fffffffff00000000000000000000000016916115618a614759565b60801b71ffff0000000000000000000000000000000016936115828c614759565b60901b73ffff00000000000000000000000000000000000016957fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016177fffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffff16177fffffffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffff16177fffffffffffffffffffffffff0000ffffffffffffffffffffffffffffffffffff1617171781556116358761474c565b81547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff0000000000000000000000000000000000000000161790556040519661168690614779565b63ffffffff16875261169790614779565b63ffffffff1660208701526116ab90614779565b63ffffffff1660408601526116bf90614779565b63ffffffff1660608501526116d39061478a565b61ffff1660808401526116e59061478a565b61ffff1660a08301526116f790613c79565b151560c082015260e07ffae1e296719dac5269c3886fb5002bb29bf17ae403060c6eb063a55abaaa104191a26001016113d0565b60248c877f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b60248c61ffff61176686614759565b7f95f3517a00000000000000000000000000000000000000000000000000000000835216600452fd5b8a61ffff611766602493614759565b60248a857f12332265000000000000000000000000000000000000000000000000000000008252600452fd5b7f1e670e4b000000000000000000000000000000000000000000000000000000008752600452602486fd5b50346106685760406003193601126106685760043567ffffffffffffffff81116107f057611827903690600401613e58565b90611830613b65565b9173ffffffffffffffffffffffffffffffffffffffff6001541633141580611a84575b611a585773ffffffffffffffffffffffffffffffffffffffff8316908115611a3057845b818110611882578580f35b73ffffffffffffffffffffffffffffffffffffffff6118aa6118a5838588614799565b614536565b166040517f70a08231000000000000000000000000000000000000000000000000000000008152306004820152602081602481855afa908115611a255788916119f2575b50806118ff575b5050600101611877565b6040517fa9059cbb00000000000000000000000000000000000000000000000000000000602080830191825273ffffffffffffffffffffffffffffffffffffffff8a16602484015260448084018590528352918a9190611960606482613d25565b519082865af1156119e75787513d6119de5750813b155b6119b25790847f508d7d183612c18fc339b42618912b9fa3239f631dd7ec0671f950200a0fa66e602060019594604051908152a390386118f5565b602488837f5274afe7000000000000000000000000000000000000000000000000000000008252600452fd5b60011415611977565b6040513d89823e3d90fd5b905060203d8111611a1e575b611a088183613d25565b60208260009281010312610668575051386118ee565b503d6119fe565b6040513d8a823e3d90fd5b6004857f8579befe000000000000000000000000000000000000000000000000000000008152fd5b6024847fcb1afbd700000000000000000000000000000000000000000000000000000000815233600452fd5b5073ffffffffffffffffffffffffffffffffffffffff600c5416331415611853565b5034610668578060031936011261066857604051906006548083528260208101600684526020842092845b818110611bb8575050611ae692500383613d25565b8151611b0a611af482613eba565b91611b026040519384613d25565b808352613eba565b917fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0602083019301368437805b8451811015611b69578067ffffffffffffffff611b56600193886145c2565b5116611b6282866145c2565b5201611b37565b50925090604051928392602084019060208552518091526040840192915b818110611b95575050500390f35b825167ffffffffffffffff16845285945060209384019390920191600101611b87565b8454835260019485019487945060209093019201611ad1565b50346106685760206003193601126106685760043573ffffffffffffffffffffffffffffffffffffffff81168091036107f057611c0c614bc5565b7fffffffffffffffffffffffff00000000000000000000000000000000000000006003547fbaff46844acf36d6ee996f489a1a288709c4542bd33cd557770afd267d5812096040805173ffffffffffffffffffffffffffffffffffffffff84168152856020820152a1161760035580f35b503461066857602060031936011261066857611cb1611c9d610509613bc0565b604051918291602083526020830190613c1a565b0390f35b5034610668576020600319360112610668577f307cf716eade81675bea3ccb6917b0f91baa2160056765d9a83d76f819caf06a6020611cf2613a8d565b611cfa614bc5565b6002547fffffffffffffffff00000000ffffffffffffffffffffffffffffffffffffffff77ffffffff0000000000000000000000000000000000000000808460401c16169116176002557fffffffff0000000000000000000000000000000000000000000000000000000060405191168152a180f35b503461066857606060031936011261066857611d8a613b1f565b90611d93613b65565b6044359273ffffffffffffffffffffffffffffffffffffffff841680850361111857611dbd614bc5565b73ffffffffffffffffffffffffffffffffffffffff82168015611ed35794611ecd917f3f1036e85d016a93254a0b1415844f79b85424959d90ae5ad51ce8f4533fe70195967fffffffffffffffffffffffff0000000000000000000000000000000000000000600254161760025573ffffffffffffffffffffffffffffffffffffffff85167fffffffffffffffffffffffff0000000000000000000000000000000000000000600a541617600a557fffffffffffffffffffffffff0000000000000000000000000000000000000000600c541617600c556040519384938491604091949373ffffffffffffffffffffffffffffffffffffffff809281606087019816865216602085015216910152565b0390a180f35b6004857f14c880ca000000000000000000000000000000000000000000000000000000008152fd5b50346106685767ffffffffffffffff611f1336613de9565b929091611f1e614bc5565b1691611f37836000526007602052604060002054151590565b156110ec578284526008602052611f6660056040862001611f59368486613d66565b6020815191012090615715565b15611fab57907f52d00ee4d9bd51b40168f2afc5848837288ce258784ad914278791464b3f4d7691611fa5604051928392602084526020840191613f7b565b0390a280f35b82611fef836040519384937f74f23c7c0000000000000000000000000000000000000000000000000000000085526004850152604060248501526044840191613f7b565b0390fd5b50346106685760206003193601126106685767ffffffffffffffff612016613bc0565b168152600860205261202d6005604083200161551c565b80517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe061207261205c83613eba565b9261206a6040519485613d25565b808452613eba565b01835b818110612149575050825b82518110156120c65780612096600192856145c2565b51855260096020526120aa60408620614658565b6120b482856145c2565b526120bf81846145c2565b5001612080565b81846040519182916020830160208452825180915260408401602060408360051b870101940192905b8282106120fe57505050500390f35b91936020612139827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc060019597998495030186528851613c1a565b96019201920185949391926120ef565b806060602080938601015201612075565b50346106685760206003193601126106685760043567ffffffffffffffff81116107f057806004019060a06003198236030112610a40576121996145a9565b506040516020936121aa8583613d25565b80825260848301916121bb83614536565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361264d57602484019477ffffffffffffffff0000000000000000000000000000000061222187614557565b60801b16604051907f2cbc26bb0000000000000000000000000000000000000000000000000000000082526004820152878160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156125d2578491612630575b506126085767ffffffffffffffff6122b487614557565b166122cc816000526007602052604060002054151590565b156125dd578773ffffffffffffffffffffffffffffffffffffffff60025416916024604051809481937fa8d87a3b00000000000000000000000000000000000000000000000000000000835260048301525afa80156125d257849061258a575b73ffffffffffffffffffffffffffffffffffffffff915016330361255e576064850135946123668661235d87614536565b6106918a614557565b73ffffffffffffffffffffffffffffffffffffffff600354169182612443575b886124136105098a8a7ff33bc26b4413b0e7f19f1ea739fdf99098c0061f1f87d954b11f5293fad9ae1067ffffffffffffffff8c6123c78461048e87614557565b6105016123dc6123d687614557565b92614536565b6040805173ffffffffffffffffffffffffffffffffffffffff90921682523360208301528101959095529116929081906060820190565b9061241c614f59565b6040519261242984613cd1565b835281830152611cb1604051928284938452830190613e2e565b823b1561070d57918791858094604051968795869485937fa8027c0f00000000000000000000000000000000000000000000000000000000855260048501608090528061248f91615416565b6084860160a090526101248601906124a692613f7b565b916124b090613bd7565b67ffffffffffffffff1660a48501526044016124cb90613b88565b73ffffffffffffffffffffffffffffffffffffffff1660c48401528b60e48401526124f58b613b88565b73ffffffffffffffffffffffffffffffffffffffff1661010484015283602484015282810360031901604484015261252c91613c1a565b8a606483015203925af1801561066b57612549575b808080612386565b612554828092613d25565b6106685780612541565b6024837f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b508781813d83116125cb575b6125a08183613d25565b81010312611118576125c673ffffffffffffffffffffffffffffffffffffffff91613f5a565b61232c565b503d612596565b6040513d86823e3d90fd5b7fa9902c7e000000000000000000000000000000000000000000000000000000008452600452602483fd5b6004837f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b6126479150883d8a1161079f576107918183613d25565b3861229d565b5073ffffffffffffffffffffffffffffffffffffffff6107c7602493614536565b5034610668578060031936011261066857602073ffffffffffffffffffffffffffffffffffffffff60015416604051908152f35b5034610668578060031936011261066857602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461066857602060031936011261066857602061272f67ffffffffffffffff61271b613bc0565b166000526007602052604060002054151590565b6040519015158152f35b5034610668578060031936011261066857805473ffffffffffffffffffffffffffffffffffffffff811633036127d8577fffffffffffffffffffffffff000000000000000000000000000000000000000060015491338284161760015516825573ffffffffffffffffffffffffffffffffffffffff3391167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08380a380f35b6004827f02b543c6000000000000000000000000000000000000000000000000000000008152fd5b5034610668578060031936011261066857600254600a54600c546040805173ffffffffffffffffffffffffffffffffffffffff94851681529284166020840152921691810191909152606090f35b50346106685761285d36613de9565b61286993929193614bc5565b67ffffffffffffffff821661288b816000526007602052604060002054151590565b156128aa57506128a792936128a1913691613d66565b90614c10565b80f35b7f1e670e4b000000000000000000000000000000000000000000000000000000008452600452602483fd5b5034610668576040600319360112610668576128ef613bc0565b906024359067ffffffffffffffff821161066857602061272f846129163660048701613dcb565b9061456c565b5034610668576020600319360112610668576004359067ffffffffffffffff82116106685781600401906101006003198436030112610668578060405161296281613c86565b528060405161297081613c86565b52606483013560c48401936129a061299a61299561298e88886144e5565b3691613d66565b614879565b83614934565b9360848201956129af87614536565b73ffffffffffffffffffffffffffffffffffffffff807f000000000000000000000000000000000000000000000000000000000000000016911603612ff857602483019377ffffffffffffffff00000000000000000000000000000000612a1586614557565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa9081156119e7578791612fd9575b50612fb15767ffffffffffffffff612aa986614557565b16612ac1816000526007602052604060002054151590565b15612f8657602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa9081156119e7578791612f67575b5015612f3b57612b3885614557565b92612b4e60a486019461291661298e87856144e5565b15612ef457612b6f88612b608b614536565b612b6989614557565b9061532d565b73ffffffffffffffffffffffffffffffffffffffff600354169283612d22575b505050505060440191612ba183614536565b612baa83614557565b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b1561111857608484928367ffffffffffffffff9373ffffffffffffffffffffffffffffffffffffffff60405197889687957f74fd18ac000000000000000000000000000000000000000000000000000000008752837f00000000000000000000000000000000000000000000000000000000000000001660048801521660248601528c60448601521660648401525af1801561066b57612d0d575b5050608067ffffffffffffffff60209573ffffffffffffffffffffffffffffffffffffffff612cd9612cd36104c97ffc5e3a5bddc11d92c2dc20fae6f7d5eb989f056be35239f7de7e86150609abc097614557565b96614536565b816040519716875233898801521660408601528560608601521692a260405190612d0282613c86565b815260405190518152f35b612d18828092613d25565b6106685780612c7e565b833b15612ef057878795938195938c93604051988997889687957f6371157400000000000000000000000000000000000000000000000000000000875260048701606090528d612d728780615416565b60648a0161010090526101648a0190612d8a92613f7b565b94612d9490613bd7565b67ffffffffffffffff166084890152604401612daf90613b88565b73ffffffffffffffffffffffffffffffffffffffff1660a488015260c4870152612dd890613b88565b73ffffffffffffffffffffffffffffffffffffffff1660e4860152612dfd9084615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c86840301610104870152612e329291613f7b565b90612e3d9083615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c85840301610124860152612e729291613f7b565b9060e48a01612e8091615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c84840301610144850152612eb59291613f7b565b8b602483015282604483015203925af180156125d257908491612edb575b808080612b8f565b81612ee591613d25565b610a40578238612ed3565b8780fd5b83612efe916144e5565b611fef6040519283927f24eb47e5000000000000000000000000000000000000000000000000000000008452602060048501526024840191613f7b565b6024867f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b612f80915060203d60201161079f576107918183613d25565b38612b29565b7fa9902c7e000000000000000000000000000000000000000000000000000000008752600452602486fd5b6004867f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b612ff2915060203d60201161079f576107918183613d25565b38612a92565b60248573ffffffffffffffffffffffffffffffffffffffff6107c78a614536565b5034610668578060031936011261066857602073ffffffffffffffffffffffffffffffffffffffff60035416604051908152f35b5034610668576040600319360112610668576004359067ffffffffffffffff821161066857816004019061010060031984360301126106685761308e613ac1565b918160405161309c81613c86565b5260648401359360c48101936130c16130bb61299561298e88876144e5565b87614934565b9460848301966130d088614536565b73ffffffffffffffffffffffffffffffffffffffff807f00000000000000000000000000000000000000000000000000000000000000001691160361359f57602484019477ffffffffffffffff0000000000000000000000000000000061313687614557565b60801b16604051907f2cbc26bb000000000000000000000000000000000000000000000000000000008252600482015260208160248173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000165afa908115611a25578891613580575b506135585767ffffffffffffffff6131ca87614557565b166131e2816000526007602052604060002054151590565b1561352d57602073ffffffffffffffffffffffffffffffffffffffff60025416916044604051809481937f83826b2b00000000000000000000000000000000000000000000000000000000835260048301523360248301525afa908115611a2557889161350e575b50156134e25761325986614557565b9361326f60a487019561291661298e88866144e5565b156134d8577fffffffff00000000000000000000000000000000000000000000000000000000169081156134bd576132b9896132aa8c614536565b6132b38a614557565b906153a6565b73ffffffffffffffffffffffffffffffffffffffff6003541693846132ec575b50505050505060440191612ba183614536565b843b156134b957868995938c959387938b6040519a8b998a9889977f63711574000000000000000000000000000000000000000000000000000000008952600489016060905261333c8780615416565b60648b0161010090526101648b019061335492613f7b565b9461335e90613bd7565b67ffffffffffffffff1660848a015260440161337990613b88565b73ffffffffffffffffffffffffffffffffffffffff1660a489015260c48801526133a290613b88565b73ffffffffffffffffffffffffffffffffffffffff1660e48701526133c79084615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c878403016101048801526133fc9291613f7b565b906134079083615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c8684030161012487015261343c9291613f7b565b9060e48b0161344a91615416565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c8584030161014486015261347f9291613f7b565b908c6024840152604483015203925af180156125d2576134a4575b80808080806132d9565b926134b28160449395613d25565b929061349a565b8880fd5b6134d3896134ca8c614536565b612b698a614557565b6132b9565b612efe85836144e5565b6024877f728fe07b00000000000000000000000000000000000000000000000000000000815233600452fd5b613527915060203d60201161079f576107918183613d25565b3861324a565b7fa9902c7e000000000000000000000000000000000000000000000000000000008852600452602487fd5b6004877f53ad11d8000000000000000000000000000000000000000000000000000000008152fd5b613599915060203d60201161079f576107918183613d25565b386131b3565b60248673ffffffffffffffffffffffffffffffffffffffff6107c78b614536565b5034610668578060031936011261066857602060405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b503461066857604060031936011261066857613618613bc0565b602435918215158303610668576101406136db6136358585614462565b61368b60409392935180946fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565b60a08301906fffffffffffffffffffffffffffffffff6080809282815116855263ffffffff6020820151166020860152604081015115156040860152826060820151166060860152015116910152565bf35b5034610668576020600319360112610668576020906136fa613b1f565b905073ffffffffffffffffffffffffffffffffffffffff807f0000000000000000000000000000000000000000000000000000000000000000169116146040519015158152f35b5034610668578060031936011261066857602060405173ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b50346106685760c0600319360112610668576137ac613b1f565b506137b5613ba9565b6137bd613b42565b50608435917fffffffff00000000000000000000000000000000000000000000000000000000831683036106685760a4359067ffffffffffffffff82116106685760a063ffffffff8061ffff613822888861381b3660048b01613bec565b50506142b2565b95926040979194975197885216602087015216604085015216606083015215156080820152f35b503461066857806003193601126106685750611cb160405161386c604082613d25565b601a81527f4c6f636b52656c65617365546f6b656e506f6f6c20322e302e300000000000006020820152604051918291602083526020830190613c1a565b50346106685760c0600319360112610668576138c4613b1f565b6138cc613ba9565b906064357fffffffff00000000000000000000000000000000000000000000000000000000811681036111185760843567ffffffffffffffff811161070d57613919903690600401613bec565b9160a435936002851015610676576139349560443591613fba565b90604051918291602083016020845282518091526020604085019301915b818110613960575050500390f35b825173ffffffffffffffffffffffffffffffffffffffff16845285945060209384019390920191600101613952565b9050346107f05760206003193601126107f0576020907fffffffff000000000000000000000000000000000000000000000000000000006139ce613a8d565b167faff2afbf000000000000000000000000000000000000000000000000000000008114908115613a63575b8115613a39575b8115613a0f575b5015158152f35b7f01ffc9a70000000000000000000000000000000000000000000000000000000091501483613a08565b7f0e64dd290000000000000000000000000000000000000000000000000000000081149150613a01565b7f940a154200000000000000000000000000000000000000000000000000000000811491506139fa565b600435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613abc57565b600080fd5b602435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613abc57565b604435907fffffffff0000000000000000000000000000000000000000000000000000000082168203613abc57565b6004359073ffffffffffffffffffffffffffffffffffffffff82168203613abc57565b6064359073ffffffffffffffffffffffffffffffffffffffff82168203613abc57565b6024359073ffffffffffffffffffffffffffffffffffffffff82168203613abc57565b359073ffffffffffffffffffffffffffffffffffffffff82168203613abc57565b6024359067ffffffffffffffff82168203613abc57565b6004359067ffffffffffffffff82168203613abc57565b359067ffffffffffffffff82168203613abc57565b9181601f84011215613abc5782359167ffffffffffffffff8311613abc5760208381860195010111613abc57565b919082519283825260005b848110613c645750507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8460006020809697860101520116010190565b80602080928401015182828601015201613c25565b35908115158203613abc57565b6020810190811067ffffffffffffffff821117613ca257604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040810190811067ffffffffffffffff821117613ca257604052565b60a0810190811067ffffffffffffffff821117613ca257604052565b60e0810190811067ffffffffffffffff821117613ca257604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff821117613ca257604052565b92919267ffffffffffffffff8211613ca25760405191613dae601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01660200184613d25565b829481845281830111613abc578281602093846000960137010152565b9080601f83011215613abc57816020613de693359101613d66565b90565b906040600319830112613abc5760043567ffffffffffffffff81168103613abc57916024359067ffffffffffffffff8211613abc57613e2a91600401613bec565b9091565b613de6916020613e478351604084526040840190613c1a565b920151906020818403910152613c1a565b9181601f84011215613abc5782359167ffffffffffffffff8311613abc576020808501948460051b010111613abc57565b9181601f84011215613abc5782359167ffffffffffffffff8311613abc576020808501948460081b010111613abc57565b67ffffffffffffffff8111613ca25760051b60200190565b81810292918115918404141715613ee557565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b8115613f1e570490565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b91908203918211613ee557565b519073ffffffffffffffffffffffffffffffffffffffff82168203613abc57565b601f82602094937fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0938186528686013760008582860101520116010190565b92959390919473ffffffffffffffffffffffffffffffffffffffff6003541695861561429057809760028710156142615773ffffffffffffffffffffffffffffffffffffffff9861411b957fffffffff0000000000000000000000000000000000000000000000000000000093896142375767ffffffffffffffff8216600052600b6020526040600020906040519161405283613d09565b549163ffffffff8316815263ffffffff8360201c16602082015263ffffffff8360401c16604082015263ffffffff8360601c16606082015260c061ffff8460801c169182608082015260ff60a082019561ffff8160901c16875260a01c16151591829101526141e3575b50505067ffffffffffffffff905b6040519b8c997f06b859ef000000000000000000000000000000000000000000000000000000008b521660048a0152166024880152604487015216606485015260c0608485015260c4840191613f7b565b928180600095869560a483015203915afa9182156141d657819261413e57505090565b9091503d8083833e6141508183613d25565b810190602081830312610a405780519067ffffffffffffffff8211611118570181601f82011215610a405780519061418782613eba565b936141956040519586613d25565b82855260208086019360051b8301019384116106685750602001905b8282106141be5750505090565b602080916141cb84613f5a565b8152019101906141b1565b50604051903d90823e3d90fd5b92935067ffffffffffffffff928587161561421f575061271061420e61ffff61421594511683613ed2565b0490613f4d565b915b9038806140bc565b614231925061420e6127109183613ed2565b91614217565b67ffffffffffffffff91925061425b9061425561299536898b613d66565b90614934565b916140ca565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602160045260246000fd5b50505050505050506040516142a6602082613d25565b60008152600036813790565b67ffffffffffffffff909291926142f07fffffffff0000000000000000000000000000000000000000000000000000000060025460401b1685614a41565b16600052600b60205260406000206040519061430b82613d09565b549163ffffffff83169384835263ffffffff8460201c169384602085015263ffffffff8160401c169182604086015263ffffffff8260601c169081606087015261ffff8360801c169586608082015260ff61ffff8560901c16948560a084015260a01c16159060c082159101526143b8577fffffffff00000000000000000000000000000000000000000000000000000000166143ad57505093929190600190565b959493509160019150565b5050505092505050600090600090600090600090600090565b604051906143de82613ced565b60006080838281528260208201528260408201528260608201520152565b9060405161440981613ced565b60806001829460ff81546fffffffffffffffffffffffffffffffff8116865263ffffffff81861c16602087015260a01c161515604085015201546fffffffffffffffffffffffffffffffff81166060840152811c910152565b67ffffffffffffffff916144746143d1565b5061447d6143d1565b506144b157166000526008602052604060002090613de66144a560026144aa6144a5866143fc565b614b28565b94016143fc565b16908160005260046020526144cc6144a560406000206143fc565b916000526005602052613de66144a560406000206143fc565b9035907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe181360301821215613abc570180359067ffffffffffffffff8211613abc57602001918136038313613abc57565b3573ffffffffffffffffffffffffffffffffffffffff81168103613abc5790565b3567ffffffffffffffff81168103613abc5790565b9067ffffffffffffffff613de692166000526008602052600560406000200190602081519101209060019160005201602052604060002054151590565b604051906145b682613cd1565b60606020838281520152565b80518210156145d65760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90600182811c9216801561464e575b602083101461461f57565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b91607f1691614614565b906040519182600082549261466c84614605565b80845293600181169081156146da5750600114614693575b5061469192500383613d25565b565b90506000929192526020600020906000915b8183106146be5750509060206146919282010138614684565b60209193508060019154838589010152019101909184926146a5565b602093506146919592507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0091501682840152151560051b82010138614684565b67ffffffffffffffff166000526008602052613de66004604060002001614658565b91908110156145d65760081b0190565b358015158103613abc5790565b3561ffff81168103613abc5790565b3563ffffffff81168103613abc5790565b359063ffffffff82168203613abc57565b359061ffff82168203613abc57565b91908110156145d65760051b0190565b35906fffffffffffffffffffffffffffffffff82168203613abc57565b9190826060910312613abc576040516060810181811067ffffffffffffffff821117613ca257604052604061481981839561480081613c79565b855261480e602082016147a9565b6020860152016147a9565b910152565b6fffffffffffffffffffffffffffffffff61485c6040809361483f81613c79565b1515865283614850602083016147a9565b166020870152016147a9565b16910152565b81811061486d575050565b60008155600101614862565b805180156148e9576020036148ab578051602082810191830183900312613abc57519060ff82116148ab575060ff1690565b611fef906040519182917f953576f7000000000000000000000000000000000000000000000000000000008352602060048401526024830190613c1a565b50507f000000000000000000000000000000000000000000000000000000000000000090565b9060ff8091169116039060ff8211613ee557565b60ff16604d8111613ee557600a0a90565b907f00000000000000000000000000000000000000000000000000000000000000009060ff82169060ff811692828414614a3a57828411614a1057906149799161490f565b91604d60ff84161180156149d7575b6149a15750509061499b613de692614923565b90613ed2565b9091507fa9cb113d0000000000000000000000000000000000000000000000000000000060005260045260245260445260646000fd5b506149e183614923565b8015613f1e577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048411614988565b614a199161490f565b91604d60ff8416116149a157505090614a34613de692614923565b90613f14565b5050505090565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115614b2357614a7481615252565b7dffff00000000000000000000000000000000000000000000000000000000601082811c9085901c1616614b235761ffff8360e01c168015918215614b12575b5050614abe575050565b7fffffffff0000000000000000000000000000000000000000000000000000000092507fdf63778f000000000000000000000000000000000000000000000000000000006000526004521660245260446000fd5b60e01c61ffff161090503880614ab4565b505050565b614b306143d1565b506fffffffffffffffffffffffffffffffff6060820151166fffffffffffffffffffffffffffffffff8083511691614b8d6020850193614b87614b7a63ffffffff87511642613f4d565b8560808901511690613ed2565b90615245565b80821015614ba657505b16825263ffffffff4216905290565b9050614b97565b90816020910312613abc57518015158103613abc5790565b73ffffffffffffffffffffffffffffffffffffffff600154163303614be657565b7f2b5c74de0000000000000000000000000000000000000000000000000000000060005260046000fd5b90805115614e505767ffffffffffffffff81516020830120921691826000526008602052614c458160056040600020016158a9565b15614e0c5760005260096020526040600020815167ffffffffffffffff8111613ca257614c728254614605565b601f8111614dda575b506020601f8211600114614d145791614cee827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea9593614d0495600091614d09575b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8260011b9260031b1c19161790565b9055604051918291602083526020830190613c1a565b0390a2565b905084015138614cbd565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe082169083600052806000209160005b818110614dc2575092614d049492600192827f7d628c9a1796743d365ab521a8b2a4686e419b3269919dc9145ea2ce853b54ea989610614d8b575b5050811b019055611c9d565b8501517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60f88460031b161c191690553880614d7f565b9192602060018192868a015181550194019201614d44565b614e0690836000526020600020601f840160051c81019160208510610e9057601f0160051c0190614862565b38614c7b565b5090611fef6040519283927f393b8ad20000000000000000000000000000000000000000000000000000000084526004840152604060248401526044830190613c1a565b7f14c880ca0000000000000000000000000000000000000000000000000000000060005260046000fd5b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016803b15613abc5767ffffffffffffffff906064604051809481937fa36a7fee0000000000000000000000000000000000000000000000000000000083526000978896879373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016600487015216602485015260448401525af1801561066b57614f4c575050565b81614f5691613d25565b50565b60405160ff7f000000000000000000000000000000000000000000000000000000000000000016602082015260208152613de6604082613d25565b815191929115615116576fffffffffffffffffffffffffffffffff6040840151166fffffffffffffffffffffffffffffffff602085015116106150b35761469191925b805182547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff1690151560a01b74ff0000000000000000000000000000000000000000161782556020810151825460409290920151608090811b7fffffffffffffffffffffffffffffffff00000000000000000000000000000000166fffffffffffffffffffffffffffffffff9290921691821760018501557fffffffffffffffffffffffff0000000000000000000000000000000000000000909216174290911b73ffffffff0000000000000000000000000000000016179055565b606483615114604051917f8020d12400000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565bfd5b6fffffffffffffffffffffffffffffffff604084015116158015906151a5575b615144576146919192614fd7565b606483615114604051917fd68af9cc00000000000000000000000000000000000000000000000000000000835260048301906fffffffffffffffffffffffffffffffff60408092805115158552826020820151166020860152015116910152565b506fffffffffffffffffffffffffffffffff6020840151161515615136565b906127109167ffffffffffffffff6151de60208301614557565b166000908152600b60205260409020917fffffffff00000000000000000000000000000000000000000000000000000000161561522f57606061ffff61522b935460901c16910135613ed2565b0490565b606061ffff61522b935460801c16910135613ed2565b91908201809211613ee557565b7fffffffff000000000000000000000000000000000000000000000000000000008116908115615329577dffff000000000000000000000000000000000000000000000000000000008116156153205760ff60015b169060f01c806152ea575b506001036152bd5750565b7fc512f96c0000000000000000000000000000000000000000000000000000000060005260045260246000fd5b60005b601081106152fb57506152b2565b6001811b821661530e575b6001016152ed565b9160018101809111613ee55791615306565b60ff60006152a7565b5050565b9167ffffffffffffffff7f50f6fbee3ceedce6b7fd7eaef18244487867e6718aec7208187efb6b7908c14c921692836000526008602052615376818360026040600020016158fe565b6040805173ffffffffffffffffffffffffffffffffffffffff909216825260208201929092529081908101614d04565b91909167ffffffffffffffff83169283600052600560205260ff60406000205460a01c161561540b5750907fc6735cd4fa2bbe7b203b1682936e6ee61bc1702464bbbd12abb6630229d9a5f991836000526005602052615376818360406000206158fe565b90614691935061532d565b90357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe182360301811215613abc57016020813591019167ffffffffffffffff8211613abc578136038313613abc57565b9167ffffffffffffffff7fff0133389f9bb82d5b9385826160eaf2328039f6fa950eeb8cf0836da8178944921692836000526008602052615376818360406000206158fe565b91909167ffffffffffffffff83169283600052600460205260ff60406000205460a01c16156155115750907f28d6c52e2b0b7587b0d195539fbe6af984b28791aca4d2cc0844244e38bce29e91836000526004602052615376818360406000206158fe565b906146919350615466565b906040519182815491828252602082019060005260206000209260005b81811061554e57505061469192500383613d25565b8454835260019485019487945060209093019201615539565b80548210156145d65760005260206000200190600090565b600081815260076020526040902054801561570e577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613ee557600654907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613ee55781810361569f575b5050506006548015615670577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0161562d816006615567565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b19169055600655600052600760205260006040812055600190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603160045260246000fd5b6156f66156b06156c1936006615567565b90549060031b1c9283926006615567565b81939154907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9060031b92831b921b19161790565b905560005260076020526040600020553880806155f4565b5050600090565b9060018201918160005282602052604060002054801515600014615840577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8101818111613ee5578254907fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8201918211613ee557818103615809575b50505080548015615670577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01906157ca8282615567565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82549160031b1b191690555560005260205260006040812055600190565b6158296158196156c19386615567565b90549060031b1c92839286615567565b905560005283602052604060002055388080615792565b50505050600090565b806000526007602052604060002054156000146158a35760065468010000000000000000811015613ca25761588a6156c18260018594016006556006615567565b9055600654906000526007602052604060002055600190565b50600090565b600082815260018201602052604090205461570e5780549068010000000000000000821015613ca257826158e76156c1846001809601855584615567565b905580549260005201602052604060002055600190565b9182549060ff8260a01c16158015615bb2575b615bac576fffffffffffffffffffffffffffffffff8216916001850190815461595663ffffffff6fffffffffffffffffffffffffffffffff83169360801c1642613f4d565b9081615b0e575b5050848110615ac257508383106159b757505061598c6fffffffffffffffffffffffffffffffff928392613f4d565b16167fffffffffffffffffffffffffffffffff00000000000000000000000000000000825416179055565b9190915460801c928315615a5657816159cf91613f4d565b927fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff810190808211613ee557615a1d615a229273ffffffffffffffffffffffffffffffffffffffff96615245565b613f14565b7fd0c8d23a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b73ffffffffffffffffffffffffffffffffffffffff83837fd0c8d23a000000000000000000000000000000000000000000000000000000006000527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6004526024521660445260646000fd5b828573ffffffffffffffffffffffffffffffffffffffff927f1a76572a000000000000000000000000000000000000000000000000000000006000526004526024521660445260646000fd5b828692939611615b8257615b2992614b879160801c90613ed2565b80841015615b7d5750825b85547fffffffffffffffffffffffff00000000ffffffffffffffffffffffffffffffff164260801b73ffffffff000000000000000000000000000000001617865592388061595d565b615b34565b7f9725942a0000000000000000000000000000000000000000000000000000000060005260046000fd5b50505050565b50821561591156fea164736f6c634300081a000a' as const +// generate:end diff --git a/ccip-sdk/src/cct/evm/index.test.ts b/ccip-sdk/src/cct/evm/index.test.ts new file mode 100644 index 00000000..5df1639b --- /dev/null +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -0,0 +1,725 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, id } from 'ethers' + +import { EVMTokenManager } from './index.ts' +import { CCIPWalletInvalidError } from '../../errors/index.ts' +import { interfaces } from '../../evm/const.ts' +import type { EVMChain } from '../../evm/index.ts' +import { ChainFamily } from '../../networks.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../errors.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const POOL = '0x' + '22'.repeat(20) +const ROUTER = '0x' + '33'.repeat(20) +const TAR = '0x' + '44'.repeat(20) +const REGISTRY_MODULE = '0x' + '55'.repeat(20) +const ADMIN = '0x' + '66'.repeat(20) +// Distinct from ADMIN/REGISTRY_MODULE on purpose: sharing a value would let an assertion pass +// against the wrong address. +const CURRENT_ADMIN = '0x' + '77'.repeat(20) +const NEW_ADMIN = '0x' + '88'.repeat(20) + +/** Encodes a `getTokenConfig` result the way the on-chain TAR would. */ +function encodeTokenConfig(administrator: string, pendingAdministrator = ZeroAddress) { + return interfaces.TokenAdminRegistry.encodeFunctionResult('getTokenConfig', [ + [administrator, pendingAdministrator, ZeroAddress], + ]) +} + +/** Minimal EVMChain stub — only the members EVMTokenManager touches. */ +function stubChain(overrides: Partial = {}, poolVersion = '1.5.1'): EVMChain { + return { + provider: { call: async () => encodeTokenConfig(CURRENT_ADMIN) }, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + getTokenAdminRegistryFor: (_address: string) => Promise.resolve(TAR), + typeAndVersion: (address: string) => + Promise.resolve( + address === REGISTRY_MODULE + ? ['RegistryModuleOwnerCustom', '1.6.0'] + : ['BurnMintTokenPool', poolVersion], + ), + nextNonce: async () => 0, + rollbackNonce: () => {}, + ...overrides, + } as unknown as EVMChain +} + +const HASH = '0x' + 'ab'.repeat(32) + +/** Fake ethers Signer whose broadcast resolves to a confirmed receipt. */ +function fakeSigner(address = TOKEN) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ hash: HASH, wait: () => Promise.resolve({ status: 1 }) }), + } +} + +const REGISTER_ADMIN_SELECTOR = id('registerAdminViaOwner(address)').slice(0, 10) +const IS_REGISTRY_MODULE_SELECTOR = + interfaces.TokenAdminRegistry.getFunction('isRegistryModule')!.selector +const GET_TOKEN_CONFIG_SELECTOR = + interfaces.TokenAdminRegistry.getFunction('getTokenConfig')!.selector +const OWNER_SELECTOR = new Interface(['function owner() view returns (address)']).getFunction( + 'owner', +)!.selector + +/** + * Selector-aware `provider.call` for `registerAdmin`'s on-chain checks: the module is + * registered, the token is unregistered, and `owner()` resolves to `ADMIN`. + */ +function registerAdminProvider() { + return { + call: async (tx: { data?: string }) => { + const sel = (tx.data ?? '0x').slice(0, 10) + if (sel === IS_REGISTRY_MODULE_SELECTOR) + return interfaces.TokenAdminRegistry.encodeFunctionResult('isRegistryModule', [true]) + if (sel === GET_TOKEN_CONFIG_SELECTOR) + return interfaces.TokenAdminRegistry.encodeFunctionResult('getTokenConfig', [ + [ZeroAddress, ZeroAddress, ZeroAddress], + ]) + if (sel === OWNER_SELECTOR) + return new Interface(['function owner() view returns (address)']).encodeFunctionResult( + 'owner', + [ADMIN], + ) + throw new Error(`registerAdminProvider: unexpected call, selector ${sel}`) + }, + } +} + +const SET_POOL_SELECTOR = id('setPool(address,address)').slice(0, 10) +const TRANSFER_ADMIN_ROLE_SELECTOR = id('transferAdminRole(address,address)').slice(0, 10) +const EXPECTED_TRANSFER_ADMIN = new Interface([ + 'function transferAdminRole(address localToken, address newAdmin)', +]).encodeFunctionData('transferAdminRole', [TOKEN, NEW_ADMIN]) +const EXPECTED_DATA = new Interface([ + 'function setPool(address localToken, address pool)', +]).encodeFunctionData('setPool', [TOKEN, POOL]) +const EXPECTED_TRANSFER = new Interface([ + 'function transferOwnership(address to)', +]).encodeFunctionData('transferOwnership', [TOKEN]) +const ACCEPT_ADMIN_SELECTOR = id('acceptAdminRole(address)').slice(0, 10) +const EXPECTED_ACCEPT_ADMIN = new Interface([ + 'function acceptAdminRole(address localToken)', +]).encodeFunctionData('acceptAdminRole', [TOKEN]) + +/** Fake provider whose `call` answers `getTokenConfig` with `pendingAdministrator = TOKEN`. */ +function acceptAdminProvider(pendingAdministrator: string) { + return { + call: () => + Promise.resolve( + interfaces.TokenAdminRegistry.encodeFunctionResult('getTokenConfig', [ + [ZeroAddress, pendingAdministrator, ZeroAddress], + ]), + ), + } +} + +describe('EVMTokenManager (cct/evm)', () => { + describe('construction', () => { + it('fromChain wraps an existing chain and exposes its provider', () => { + const chain = stubChain() + const cct = EVMTokenManager.fromChain(chain) + assert.ok(cct instanceof EVMTokenManager) + assert.equal(cct.chain, chain) + assert.equal(cct.provider, chain.provider) + }) + }) + + describe('generateUnsignedRegisterAdmin', () => { + it('encodes registerAdminViaOwner(token) to the registry module', async () => { + const cct = EVMTokenManager.fromChain( + stubChain({ provider: registerAdminProvider() as never }), + ) + const unsigned = await cct.generateUnsignedRegisterAdmin({ + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + sender: ADMIN, + }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal(tx.to, REGISTRY_MODULE) + assert.equal(tx.from, ADMIN) + assert.ok( + tx.data!.startsWith(REGISTER_ADMIN_SELECTOR), + 'data starts with registerAdminViaOwner selector', + ) + }) + + it('rejects an invalid address before any RPC, tagged with the operation', async () => { + let called = false + const cct = EVMTokenManager.fromChain( + stubChain({ + getTokenAdminRegistryFor: () => { + called = true + return Promise.resolve(TAR) + }, + }), + ) + await assert.rejects( + () => + cct.generateUnsignedRegisterAdmin({ + tokenAddress: 'not-an-address', + registryModule: REGISTRY_MODULE, + address: ROUTER, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'registerAdmin' && + err.context.param === 'tokenAddress', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + }) + + describe('registerAdmin', () => { + it('signs and submits, resolving to the confirmed tx hash', async () => { + const cct = EVMTokenManager.fromChain( + stubChain({ provider: registerAdminProvider() as never }), + ) + // `sender` is left off `opts` — `registerAdmin` defaults it to the wallet's own address + // (see `RegisterAdmin.execute`), which must equal `owner()` (ADMIN, per `registerAdminProvider`). + const result = await cct.registerAdmin({ + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + wallet: fakeSigner(ADMIN), + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-signer wallet', async () => { + const cct = EVMTokenManager.fromChain( + stubChain({ provider: registerAdminProvider() as never }), + ) + await assert.rejects( + () => + cct.registerAdmin({ + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + wallet: {}, + }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + + it('rejects a wallet that is not the token owner before any tx is submitted', async () => { + const cct = EVMTokenManager.fromChain( + stubChain({ provider: registerAdminProvider() as never }), + ) + // No explicit `sender` — this is the default `registerAdmin({ ...params, wallet })` shape, + // the exact path the authority check must not skip (it defaults `sender` to the wallet's + // own address, so a wallet that isn't `owner()` is caught here, pre-tx). + await assert.rejects( + () => + cct.registerAdmin({ + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + wallet: fakeSigner(), // TOKEN address, not ADMIN — not the token's owner() + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'registerAdmin' && + err.context.param === 'sender', + ) + }) + }) + + describe('generateUnsignedSetPool', () => { + it('encodes setPool(token, pool) to the discovered TAR', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + const unsigned = await cct.generateUnsignedSetPool({ + tokenAddress: TOKEN, + poolAddress: POOL, + address: ROUTER, + sender: TOKEN, + }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + + const tx = unsigned.transactions[0]! + assert.equal(tx.to, TAR) + assert.equal(tx.from, TOKEN) + assert.ok(tx.data!.startsWith(SET_POOL_SELECTOR), 'data starts with setPool selector') + assert.equal(tx.data, EXPECTED_DATA) + }) + + it('discovers the TAR from the router address', async () => { + let seen: string | undefined + const cct = EVMTokenManager.fromChain( + stubChain({ + getTokenAdminRegistryFor: (address: string) => { + seen = address + return Promise.resolve(TAR) + }, + }), + ) + await cct.generateUnsignedSetPool({ + tokenAddress: TOKEN, + poolAddress: POOL, + address: ROUTER, + }) + assert.equal(seen, ROUTER) + }) + + it('omits `from` when no sender is given', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + const unsigned = await cct.generateUnsignedSetPool({ + tokenAddress: TOKEN, + poolAddress: POOL, + address: ROUTER, + }) + assert.equal(unsigned.transactions[0]!.from, undefined) + }) + + it('rejects an invalid address before any RPC, tagged with the operation', async () => { + let called = false + const cct = EVMTokenManager.fromChain( + stubChain({ + getTokenAdminRegistryFor: () => { + called = true + return Promise.resolve(TAR) + }, + }), + ) + await assert.rejects( + () => + cct.generateUnsignedSetPool({ + tokenAddress: 'not-an-address', + poolAddress: POOL, + address: ROUTER, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setPool' && + err.context.param === 'tokenAddress', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + }) + + describe('setPool', () => { + it('signs and submits, resolving to the confirmed tx hash', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + const result = await cct.setPool({ + tokenAddress: TOKEN, + poolAddress: POOL, + address: ROUTER, + wallet: fakeSigner(), + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-signer wallet', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + await assert.rejects( + () => + cct.setPool({ + tokenAddress: TOKEN, + poolAddress: POOL, + address: ROUTER, + wallet: {}, + }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) + + describe('generateUnsignedTransferAdmin', () => { + it('encodes transferAdminRole(token, newAdmin) to the discovered TAR', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + const unsigned = await cct.generateUnsignedTransferAdmin({ + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + address: ROUTER, + sender: CURRENT_ADMIN, + }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + + const tx = unsigned.transactions[0]! + assert.equal(tx.to, TAR) + assert.equal(tx.from, CURRENT_ADMIN) + assert.ok( + tx.data!.startsWith(TRANSFER_ADMIN_ROLE_SELECTOR), + 'data starts with transferAdminRole selector', + ) + assert.equal(tx.data, EXPECTED_TRANSFER_ADMIN) + }) + + it('rejects a sender that is not the current registry administrator', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + await assert.rejects( + () => + cct.generateUnsignedTransferAdmin({ + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + address: ROUTER, + sender: NEW_ADMIN, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'transferAdmin' && + err.context.param === 'sender', + ) + }) + }) + + describe('transferAdmin', () => { + it('signs and submits, resolving to the confirmed tx hash', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + const result = await cct.transferAdmin({ + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + address: ROUTER, + sender: CURRENT_ADMIN, + wallet: fakeSigner(CURRENT_ADMIN), + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-signer wallet', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + await assert.rejects( + () => + cct.transferAdmin({ + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + address: ROUTER, + sender: CURRENT_ADMIN, + wallet: {}, + }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) + + describe('transferOwnership', () => { + it('probes the pool type/version, then builds transferOwnership to the pool', async () => { + const probed: string[] = [] + const cct = EVMTokenManager.fromChain( + stubChain({ + typeAndVersion: ((address: string) => { + probed.push(address) + return Promise.resolve(['BurnMintTokenPool', '1.5.1', 'BurnMintTokenPool 1.5.1']) + }) as unknown as EVMChain['typeAndVersion'], + }), + ) + const unsigned = await cct.generateUnsignedTransferOwnership({ + poolAddress: POOL, + newOwner: TOKEN, + }) + assert.deepEqual(probed, [POOL]) // resolved the pool's type/version from its own address + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, EXPECTED_TRANSFER) + }) + + it('surfaces an unsupported pool type reported by the probe', async () => { + const cct = EVMTokenManager.fromChain( + stubChain({ + typeAndVersion: (() => + Promise.resolve([ + 'NotATokenPool', + '1.5.1', + 'NotATokenPool 1.5.1', + ])) as unknown as EVMChain['typeAndVersion'], + }), + ) + await assert.rejects( + cct.generateUnsignedTransferOwnership({ poolAddress: POOL, newOwner: TOKEN }), + (err: unknown) => err instanceof CCTContractTypeInvalidError, + ) + }) + }) + + describe('getTokenPoolState', () => { + it('reads through the wrapped chain', async () => { + const probed: string[] = [] + const cct = EVMTokenManager.fromChain( + stubChain({ + typeAndVersion: ((address: string) => { + probed.push(address) + return Promise.resolve(['BurnMintTokenPool', '2.0.0', 'BurnMintTokenPool 2.0.0']) + }) as unknown as EVMChain['typeAndVersion'], + }), + ) + + // the pool getters themselves need a real provider, so this rejects after the probe + await assert.rejects(cct.getTokenPoolState({ poolAddress: POOL })) + assert.deepEqual(probed, [POOL], 'probes the requested pool on the wrapped chain') + }) + + it('rejects an invalid pool address before any RPC, tagged with the operation', async () => { + let probed = false + const cct = EVMTokenManager.fromChain( + stubChain({ + typeAndVersion: (() => { + probed = true + return Promise.resolve(['BurnMintTokenPool', '2.0.0', 'BurnMintTokenPool 2.0.0']) + }) as unknown as EVMChain['typeAndVersion'], + }), + ) + + await assert.rejects( + () => cct.getTokenPoolState({ poolAddress: 'not-an-address' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'getTokenPoolState' && + err.context.param === 'poolAddress', + ) + assert.equal(probed, false, 'validation fails before the typeAndVersion probe') + }) + }) + describe('generateUnsignedAcceptAdmin', () => { + it('encodes acceptAdminRole(token) to the discovered TAR when sender is pending', async () => { + const cct = EVMTokenManager.fromChain( + stubChain({ provider: acceptAdminProvider(TOKEN) as never }), + ) + const unsigned = await cct.generateUnsignedAcceptAdmin({ + tokenAddress: TOKEN, + address: ROUTER, + sender: TOKEN, + }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + + const tx = unsigned.transactions[0]! + assert.equal(tx.to, TAR) + assert.equal(tx.from, TOKEN) + assert.ok( + tx.data!.startsWith(ACCEPT_ADMIN_SELECTOR), + 'data starts with acceptAdminRole selector', + ) + assert.equal(tx.data, EXPECTED_ACCEPT_ADMIN) + }) + + it('rejects an invalid address before any RPC, tagged with the operation', async () => { + let called = false + const cct = EVMTokenManager.fromChain( + stubChain({ + getTokenAdminRegistryFor: () => { + called = true + return Promise.resolve(TAR) + }, + }), + ) + await assert.rejects( + () => + cct.generateUnsignedAcceptAdmin({ + tokenAddress: 'not-an-address', + address: ROUTER, + sender: TOKEN, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'acceptAdmin' && + err.context.param === 'tokenAddress', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + + it('rejects when sender is not the pending administrator', async () => { + const cct = EVMTokenManager.fromChain( + stubChain({ provider: acceptAdminProvider(POOL) as never }), + ) + await assert.rejects( + cct.generateUnsignedAcceptAdmin({ + tokenAddress: TOKEN, + address: ROUTER, + sender: TOKEN, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'acceptAdmin' && + err.context.param === 'sender', + ) + }) + }) + + describe('acceptAdmin', () => { + it('signs and submits, resolving to the confirmed tx hash', async () => { + const cct = EVMTokenManager.fromChain( + stubChain({ provider: acceptAdminProvider(TOKEN) as never }), + ) + const result = await cct.acceptAdmin({ + tokenAddress: TOKEN, + address: ROUTER, + sender: TOKEN, + wallet: fakeSigner(), + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-signer wallet', async () => { + const cct = EVMTokenManager.fromChain( + stubChain({ provider: acceptAdminProvider(TOKEN) as never }), + ) + await assert.rejects( + () => + cct.acceptAdmin({ + tokenAddress: TOKEN, + address: ROUTER, + sender: TOKEN, + wallet: {}, + }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + + it('rejects a sender that does not match the executing wallet', async () => { + // fakeSigner().getAddress() resolves to TOKEN; a `sender` other than TOKEN must be + // rejected rather than silently accepted and broadcast from the mismatched wallet. + const cct = EVMTokenManager.fromChain( + stubChain({ provider: acceptAdminProvider(TOKEN) as never }), + ) + await assert.rejects( + () => + cct.acceptAdmin({ + tokenAddress: TOKEN, + address: ROUTER, + sender: POOL, + wallet: fakeSigner(), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'acceptAdmin' && + err.context.param === 'sender', + ) + }) + }) + describe('getTokenAdminRegistry', () => { + const GET_TOKEN_CONFIG_IFACE = new Interface([ + 'function getTokenConfig(address token) view returns (tuple(address administrator, address pendingAdministrator, address tokenPool))', + ]) + const ADMINISTRATOR = '0x' + '77'.repeat(20) + + /** + * Chain stub whose provider answers `getTokenConfig` with `administrator`/zeroed others — + * but only for a call to `TAR` decoding to `TOKEN`. Mirrors the target/argument assertions in + * `token-admin-registry/operations/get-token-admin-registry.test.ts`'s `stubChain`: matching + * on the selector alone can't tell a correct read from one with the call target or decoded + * token swapped, since both would still reach this branch and get `encoded` back. + */ + function stubTarChain(administrator: string) { + const selector = GET_TOKEN_CONFIG_IFACE.getFunction('getTokenConfig')!.selector + const encoded = GET_TOKEN_CONFIG_IFACE.encodeFunctionResult('getTokenConfig', [ + [administrator, ZeroAddress, ZeroAddress], + ]) + return stubChain({ + provider: { + call: async ({ to, data }: { to?: string; data: string }) => { + if (data.slice(0, 10) !== selector) return '0x' + assert.equal(to, TAR, 'calls the resolved TAR, not `address`') + const [token] = GET_TOKEN_CONFIG_IFACE.decodeFunctionData('getTokenConfig', data) + assert.equal(token, TOKEN, 'reads the config for `tokenAddress`') + return encoded + }, + } as never, + }) + } + + it('reads through the wrapped chain, resolving the TAR from `address`', async () => { + const config = await EVMTokenManager.fromChain( + stubTarChain(ADMINISTRATOR), + ).getTokenAdminRegistry({ + address: ROUTER, + tokenAddress: TOKEN, + }) + assert.deepEqual(config, { administrator: ADMINISTRATOR }) + }) + + it('reports a zero administrator rather than throwing', async () => { + const config = await EVMTokenManager.fromChain( + stubTarChain(ZeroAddress), + ).getTokenAdminRegistry({ address: ROUTER, tokenAddress: TOKEN }) + assert.equal(config.administrator, ZeroAddress) + }) + + it('rejects an invalid token address before any RPC, tagged with the operation', async () => { + let called = false + const cct = EVMTokenManager.fromChain( + stubChain({ + getTokenAdminRegistryFor: () => { + called = true + return Promise.resolve(TAR) + }, + }), + ) + await assert.rejects( + () => cct.getTokenAdminRegistry({ address: ROUTER, tokenAddress: 'not-an-address' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'getTokenAdminRegistry' && + err.context.param === 'tokenAddress', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + }) + describe('getSupportedTokens', () => { + it('resolves the TAR and lists its configured tokens', async () => { + const tokens = [TOKEN, POOL] + let seenOpts: { page?: number } | undefined + const cct = EVMTokenManager.fromChain( + stubChain({ + getSupportedTokens: async (registry: string, opts?: { page?: number }) => { + assert.equal(registry, TAR) + seenOpts = opts + return tokens + }, + }), + ) + + const result = await cct.getSupportedTokens({ address: ROUTER }) + assert.deepEqual(result, tokens) + assert.deepEqual(seenOpts, { page: undefined }) + }) + + it('forwards `page` to the wrapped chain', async () => { + let seenPage: number | undefined + const cct = EVMTokenManager.fromChain( + stubChain({ + getSupportedTokens: async (_registry: string, opts?: { page?: number }) => { + seenPage = opts?.page + return [] + }, + }), + ) + + await cct.getSupportedTokens({ address: ROUTER, page: 25 }) + assert.equal(seenPage, 25) + }) + + it('rejects an invalid address before any RPC, tagged with the operation', async () => { + let called = false + const cct = EVMTokenManager.fromChain( + stubChain({ + getTokenAdminRegistryFor: () => { + called = true + return Promise.resolve(TAR) + }, + }), + ) + await assert.rejects( + () => cct.getSupportedTokens({ address: 'not-an-address' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'getSupportedTokens' && + err.context.param === 'address', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts new file mode 100644 index 00000000..b0d41bc4 --- /dev/null +++ b/ccip-sdk/src/cct/evm/index.ts @@ -0,0 +1,634 @@ +/** + * EVM Cross-Chain Token (CCT) admin operations. + * {@link EVMTokenManager} wraps an {@link EVMChain}: build with + * `generateUnsigned` (sender in opts), then `` with `wallet` in opts. + * + * @packageDocumentation + */ + +import type { JsonRpcApiProvider } from 'ethers' + +import type { ChainContext } from '../../chain.ts' +import { EVMChain } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import type { ChainFamily } from '../../networks.ts' +import type { TransactionResult } from '../operation.ts' +import { TokenManager } from '../token-manager.ts' +import { + type AuthorizeLockboxCallersParams, + AuthorizeLockboxCallers, +} from './lockbox/operations/authorize-callers.ts' +import { type DeployLockboxParams, DeployLockbox } from './lockbox/operations/deploy-lockbox.ts' +import type { DeployResult, EVMExecuteParams } from './operation.ts' +import { type DeployTokenParams, DeployToken } from './token/operations/deploy-token.ts' +import { + type AcceptAdminParams, + AcceptAdmin, +} from './token-admin-registry/operations/accept-admin.ts' +import { + type GetSupportedTokensParams, + type GetSupportedTokensResult, + GetSupportedTokens, +} from './token-admin-registry/operations/get-supported-tokens.ts' +import { + type GetTokenAdminRegistryParams, + type GetTokenAdminRegistryResult, + GetTokenAdminRegistry, +} from './token-admin-registry/operations/get-token-admin-registry.ts' +import { + type RegisterAdminParams, + RegisterAdmin, +} from './token-admin-registry/operations/register-admin.ts' +import { type SetPoolParams, SetPool } from './token-admin-registry/operations/set-pool.ts' +import { + type TransferAdminParams, + TransferAdmin, +} from './token-admin-registry/operations/transfer-admin.ts' +import { + type DeployTokenPoolParams, + DeployTokenPool, +} from './token-pool/operations/deploy-token-pool.ts' +import { + type GetTokenPoolStateParams, + type GetTokenPoolStateResult, + GetTokenPoolState, +} from './token-pool/operations/get-token-pool-state.ts' +import { + type TransferOwnershipParams, + TransferOwnership, +} from './token-pool/operations/transfer-ownership.ts' + +/** CCT admin operations for EVM chains, delegating each op to an operation class. */ +export class EVMTokenManager extends TokenManager { + readonly chain: EVMChain + // Token operations + readonly #deployToken = new DeployToken() + + // Token admin registry operations + readonly #registerAdmin = new RegisterAdmin() + readonly #setPool = new SetPool() + readonly #transferAdmin = new TransferAdmin() + readonly #acceptAdmin = new AcceptAdmin() + readonly #getTokenAdminRegistry = new GetTokenAdminRegistry() + readonly #getSupportedTokens = new GetSupportedTokens() + + // Token pool operations + readonly #deployTokenPool = new DeployTokenPool() + readonly #transferOwnership = new TransferOwnership() + readonly #getTokenPoolState = new GetTokenPoolState() + + // Lockbox operations + readonly #deployLockbox = new DeployLockbox() + readonly #authorizeLockboxCallers = new AuthorizeLockboxCallers() + + /** Wraps an {@link EVMChain}; prefer the static factory methods. */ + constructor(chain: EVMChain) { + super() + this.chain = chain + } + + /** Wraps an existing {@link EVMChain}. */ + static fromChain(chain: EVMChain): EVMTokenManager { + return new EVMTokenManager(chain) + } + + /** Creates from an ethers provider. */ + static async fromProvider( + provider: JsonRpcApiProvider, + ctx?: ChainContext, + ): Promise { + return new EVMTokenManager(await EVMChain.fromProvider(provider, ctx)) + } + + /** Creates from an RPC URL. */ + static async fromUrl(url: string, ctx?: ChainContext): Promise { + return new EVMTokenManager(await EVMChain.fromUrl(url, ctx)) + } + + /** Provider of the underlying chain. */ + get provider(): JsonRpcApiProvider { + return this.chain.provider + } + + /** + * Builds an unsigned `registerAdmin` tx (for multisig / offline signing): proposes a token's + * administrator in the TokenAdminRegistry via a RegistryModuleOwnerCustom. Two-step by design — + * the proposed administrator must then call {@link acceptAdmin}. + * @remarks The administrator is not a parameter — the module derives it on-chain. `owner`/`ccip-admin` read the token's own `owner()`/`getCCIPAdmin()`, so + * the result is independent of who signs; a wrong signer simply reverts (`CanOnlySelfRegister`). + * + * `access-control-default-admin` behaves differently and warrants care on this offline path: the + * module registers **`msg.sender`** after checking it holds the token's `DEFAULT_ADMIN_ROLE`. + * `sender` here only drives the local pre-flight probe, so if the built tx is ultimately signed + * by a *different* address that also holds that role, the **signer** becomes the token's + * administrator — silently, with no revert to catch it. Confirm the signing key before relaying + * an `access-control-default-admin` registration. {@link registerAdmin} is not exposed to this, + * since it rejects a `sender` that differs from its wallet. + * @throws {@link CCTParamsInvalidError} if any param is invalid, `registryModule` is not a + * registered TAR module, `registrationMethod` needs a v1.6+ module, `sender` doesn't match the + * token's authority for the chosen method, or the token is already registered (or pending + * acceptance) + * @example + * ```typescript + * // build only — sign later (multisig / offline). `sender` must be the token's owner (or + * // CCIP admin / default admin, matching `registrationMethod`). + * const unsigned = await cct.generateUnsignedRegisterAdmin({ + * tokenAddress: '0xToken...', + * registryModule: '0xRegistryModuleOwnerCustom...', // not discoverable on-chain + * address: '0xTokenAdminRegistry...', // the TAR, or a Router/OnRamp/OffRamp/pool to resolve it from + * sender: '0xTokenOwner...', + * }) + * ``` + */ + generateUnsignedRegisterAdmin(opts: RegisterAdminParams): Promise { + return this.#registerAdmin.generate(this.chain, opts) + } + + /** + * Proposes a token's administrator in the TokenAdminRegistry via a RegistryModuleOwnerCustom, + * signing + submitting with `opts.wallet`. Two-step by design — the proposed administrator + * must then call {@link acceptAdmin}. + * @remarks The administrator is not a parameter — see {@link generateUnsignedRegisterAdmin}. `sender` also defaults to `opts.wallet`'s address here + * (unlike the unsigned builder, where it's optional for offline/multisig flows), so the + * token-authority check always runs before this signs and submits. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid, `registryModule` is not a + * registered TAR module, `registrationMethod` needs a v1.6+ module, `sender` doesn't match the + * token's authority for the chosen method, or the token is already registered (or pending + * acceptance) + * @throws {@link CCTTxFailedError} if the tx reverts or fails + * @example + * ```typescript + * // `wallet` must be the token's owner (or CCIP admin / hold DEFAULT_ADMIN_ROLE, matching + * // `registrationMethod`) — enforced automatically since `sender` defaults to its address. + * const { hash } = await cct.registerAdmin({ + * tokenAddress: '0xToken...', + * registryModule: '0xRegistryModuleOwnerCustom...', + * address: '0xTokenAdminRegistry...', + * wallet, + * }) + * ``` + */ + registerAdmin(opts: EVMExecuteParams): Promise { + return this.#registerAdmin.execute(this.chain, opts) + } + + /** + * Builds an unsigned `setPool` tx (for multisig / offline signing). + * A zero/empty `poolAddress` delists the token from the registry. + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @example + * ```typescript + * // build only — sign later (multisig / offline). `sender` must be the token's current admin. + * const unsigned = await cct.generateUnsignedSetPool({ + * tokenAddress: '0xToken...', + * poolAddress: '0xPool...', // pass the zero address to delist the token + * address: '0xTokenAdminRegistry...', // the TAR, or a Router/pool to resolve it from + * sender: '0xTokenAdmin...', + * }) + * ``` + */ + generateUnsignedSetPool(opts: SetPoolParams): Promise { + return this.#setPool.generate(this.chain, opts) + } + + /** + * Registers a pool, signing + submitting with `opts.wallet` (the token admin). + * A zero/empty `poolAddress` delists the token from the registry. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @throws {@link CCTTxFailedError} if the tx reverts or fails + * @example + * ```typescript + * // `wallet` must sign as the token's current administrator + * const { hash } = await cct.setPool({ + * tokenAddress: '0xToken...', + * poolAddress: '0xPool...', // pass the zero address to delist the token + * address: '0xTokenAdminRegistry...', + * wallet, + * }) + * ``` + */ + setPool(opts: EVMExecuteParams): Promise { + return this.#setPool.execute(this.chain, opts) + } + + /** + * Builds an unsigned TokenAdminRegistry `transferAdmin` tx (for multisig / offline signing). + * Two-step by design: `newAdmin` must separately call `acceptAdmin` to complete the + * handoff. This is the registry's ADMIN role — distinct from a pool's Ownable2Step *owner* + * (see {@link transferOwnership}); do not confuse the two. + * @throws {@link CCTParamsInvalidError} if any param is invalid, or if `sender` is not the + * token's current registry administrator (including a not-yet-accepted registration) + * @example + * ```typescript + * // `sender` must be the token's current registry administrator + * const unsigned = await cct.generateUnsignedTransferAdmin({ + * tokenAddress: '0xToken...', + * newAdmin: '0xNewAdmin...', // must separately call acceptAdmin + * address: '0xTokenAdminRegistry...', // the TAR, or a Router/pool to resolve it from + * sender: '0xCurrentAdmin...', + * }) + * ``` + */ + generateUnsignedTransferAdmin(opts: TransferAdminParams): Promise { + return this.#transferAdmin.generate(this.chain, opts) + } + + /** + * Proposes a new TokenAdminRegistry administrator, signing + submitting with `opts.wallet` + * (the current registry admin). Two-step: `newAdmin` must separately call `acceptAdmin`. + * This is the registry's ADMIN role — distinct from a pool's Ownable2Step *owner* + * (see {@link transferOwnership}); do not confuse the two. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid, if the signing wallet is not the + * token's current registry administrator (including a not-yet-accepted registration), or if an + * explicit `opts.sender` does not match the wallet's address + * @throws {@link CCTTxFailedError} if the tx reverts or fails + * @example + * ```typescript + * // `wallet` must sign as the token's current registry administrator; `sender` defaults to its + * // address, so pass it only for offline builds via generateUnsignedTransferAdmin. + * const { hash } = await cct.transferAdmin({ + * tokenAddress: '0xToken...', + * newAdmin: '0xNewAdmin...', + * address: '0xTokenAdminRegistry...', + * wallet, + * }) + * ``` + */ + transferAdmin(opts: EVMExecuteParams): Promise { + return this.#transferAdmin.execute(this.chain, opts) + } + + /** + * Builds an unsigned `acceptAdminRole` tx (for multisig / offline signing). Second half of + * the two-step admin handshake: a registry module's `registerAdmin` (fresh registration) or + * the current admin's `transferAdmin` (hand-off) proposes `opts.sender` as + * `pendingAdministrator`; `acceptAdmin` then confirms it on-chain before encoding, after which + * {@link setPool} becomes callable by the new administrator. + * @throws {@link CCTParamsInvalidError} if any param is invalid, or `sender` is not the + * pending administrator + * @example + * ```typescript + * // `sender` must be the pending administrator proposed by registerAdmin/transferAdmin + * const unsigned = await cct.generateUnsignedAcceptAdmin({ + * tokenAddress: '0xToken...', + * address: '0xTokenAdminRegistry...', // the TAR, or a Router/pool to resolve it from + * sender: '0xPendingAdmin...', + * }) + * ``` + */ + generateUnsignedAcceptAdmin(opts: AcceptAdminParams): Promise { + return this.#acceptAdmin.generate(this.chain, opts) + } + + /** + * Accepts a pending TokenAdminRegistry administrator role, signing + submitting with + * `opts.wallet` (the pending administrator). Completes the `registerAdmin`/`transferAdmin` → + * `acceptAdmin` handshake, after which {@link setPool} becomes callable. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid, or `sender` is not the + * pending administrator + * @throws {@link CCTTxFailedError} if the tx reverts or fails + * @example + * ```typescript + * // `wallet` must sign as the pending administrator + * const { hash } = await cct.acceptAdmin({ + * tokenAddress: '0xToken...', + * address: '0xTokenAdminRegistry...', + * wallet, + * }) + * ``` + */ + acceptAdmin(opts: EVMExecuteParams): Promise { + return this.#acceptAdmin.execute(this.chain, opts) + } + + /** + * Reads a token's TokenAdminRegistry entry: its `administrator`, any `pendingAdministrator`, + * and its registered `tokenPool`. + * @remarks Deliberately diverges from `cct.chain.getRegistryTokenConfig()`, which throws when + * `administrator` is the zero address — exactly the post-`registerAdmin`, pre-`acceptAdmin` + * state. This op reports `{ administrator: ZeroAddress, pendingAdministrator }` faithfully + * instead, so a pending registration is observable; see + * {@link GetTokenAdminRegistry} for the full rationale. `pendingAdministrator` and `tokenPool` + * are still omitted when zero. + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @example + * ```typescript + * const config = await cct.getTokenAdminRegistry({ + * address: '0xTokenAdminRegistry...', // or a Router/OnRamp/OffRamp/pool to resolve it from + * tokenAddress: '0xToken...', + * }) + * if (config.administrator === ZeroAddress) { + * console.log('pending acceptance by', config.pendingAdministrator) + * } + * ``` + */ + getTokenAdminRegistry(opts: GetTokenAdminRegistryParams): Promise { + return this.#getTokenAdminRegistry.query(this.chain, opts) + } + + /** + * Lists every token configured in the TokenAdminRegistry resolved from `address`. + * @remarks The registry paginates via `getAllConfiguredTokens` — `opts.page` sets the batch size per call; omit it to read the + * whole registry in one round trip per 1000 tokens. + * @throws {@link CCTParamsInvalidError} if `address` is not a valid address, or `page` is given + * and is not a positive integer + * @example + * ```typescript + * const tokens = await cct.getSupportedTokens({ address: '0xTokenAdminRegistry...' }) + * ``` + */ + getSupportedTokens(opts: GetSupportedTokensParams): Promise { + return this.#getSupportedTokens.query(this.chain, opts) + } + + /** + * Builds an unsigned pool `transferOwnership` tx (for multisig / offline signing). Probes the + * pool's on-chain `typeAndVersion` to resolve its interface + encoder; the `transferOwnership` + * calldata is stable across pool versions, so the resolved encoding is version/type-independent. + * @throws {@link CCTParamsInvalidError} if any param is invalid + */ + generateUnsignedTransferOwnership(opts: TransferOwnershipParams): Promise { + return this.#transferOwnership.generate(this.chain, opts) + } + + /** + * Proposes a new pool owner (two-step), signing + submitting with `opts.wallet`. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @throws {@link CCTTxFailedError} if the tx reverts or fails + */ + transferOwnership(opts: EVMExecuteParams): Promise { + return this.#transferOwnership.execute(this.chain, opts) + } + + /** + * Builds an unsigned `CrossChainToken` (v2.0.0) deployment tx (for multisig / offline + * signing). The deployed address is only known once mined, so it is NOT returned here — + * use {@link deployToken} to deploy and receive `{ hash, contractAddress, verification }`. + * @remarks Same post-deploy roles caveat as {@link deployToken} — the pool needs + * `grantMintAndBurnRoles` before it can bridge. + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedDeployToken({ + * name: 'My Token', + * symbol: 'MTK', + * decimals: 18, + * maxSupply: 0n, // 0 = unlimited + * owner: '0xOwner...', // CrossChainToken v2.0.0; ccipAdmin/burnMintRoleAdmin default to owner + * sender: '0xDeployer...', + * }) + * ``` + */ + generateUnsignedDeployToken(opts: DeployTokenParams): Promise { + return this.#deployToken.generate(this.chain, opts) + } + + /** + * Deploys a `CrossChainToken` (v2.0.0), signing + submitting with `opts.wallet`; resolves + * to the tx hash, the newly deployed token address, and a `verification` + * ({@link ExplorerVerificationInput}) for verifying the source on a block explorer. + * @remarks Mint/burn are role-gated (`MINTER_ROLE`/`BURNER_ROLE`); the token grants neither + * to any pool at deploy. `preMint` mints initial supply to `preMintRecipient`, but before a + * pool can bridge, `burnMintRoleAdmin` must `grantMintAndBurnRoles(pool)`. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @throws {@link CCTTxFailedError} if the tx reverts, fails, or mines without an address + * @example + * ```typescript + * const { hash, contractAddress, verification } = await cct.deployToken({ + * name: 'My Token', + * symbol: 'MTK', + * decimals: 18, + * maxSupply: 0n, + * owner: '0xOwner...', + * wallet, + * }) + * ``` + */ + deployToken(opts: EVMExecuteParams): Promise { + return this.#deployToken.execute(this.chain, opts) + } + + /** + * Builds an unsigned pool deployment tx (for multisig / offline signing). `type` selects + * the pool contract — a `DeployableTokenPoolType` (`BurnMintTokenPool`, `BurnFromMintTokenPool`, + * `BurnWithFromMintTokenPool`, or `LockReleaseTokenPool`; all v2.0.0). The deployed address is + * only known once mined, so it is NOT returned here — use {@link deployTokenPool} to receive + * `{ hash, contractAddress, verification }`. + * @remarks Same post-deploy setup caveat as {@link deployTokenPool} — a fresh pool must be + * registered, role-granted, and lane-configured before it can bridge. `LockReleaseTokenPool` + * additionally requires a pre-deployed `lockbox` ({@link DeployLockReleaseTokenPoolParams}) + * with the pool authorized on it. The full sequence: {@link deployToken} → {@link deployLockbox} + * → {@link deployTokenPool} (passing the lockbox) → {@link authorizeLockboxCallers} + * (`addedCallers: [pool]`) → {@link setPool} → configure lanes. + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedDeployTokenPool({ + * type: 'BurnMintTokenPool', // burn-* variant; LockReleaseTokenPool additionally requires `lockbox` + * token: '0xToken...', + * localTokenDecimals: 18, + * rmnProxy: '0xRmnProxy...', + * router: '0xRouter...', + * sender: '0xDeployer...', + * }) + * ``` + */ + generateUnsignedDeployTokenPool(opts: DeployTokenPoolParams): Promise { + return this.#deployTokenPool.generate(this.chain, opts) + } + + /** + * Deploys a token pool, signing + submitting with `opts.wallet`; resolves to the tx hash, the + * newly deployed pool address, and a `verification` ({@link ExplorerVerificationInput}) for + * verifying the source on a block explorer. `type` selects the pool contract (a + * `DeployableTokenPoolType`, v2.0.0). + * @remarks Deploying the pool alone doesn't make it usable: register it with {@link setPool}, + * grant it the token's mint/burn roles (`grantMintAndBurnRoles`), and configure its remote + * pools + rate limits before it can bridge. `LockReleaseTokenPool` also needs a pre-deployed + * `lockbox` and the pool authorized on it ({@link DeployLockReleaseTokenPoolParams}). The full + * sequence: {@link deployToken} → {@link deployLockbox} → {@link deployTokenPool} (passing the + * lockbox) → {@link authorizeLockboxCallers} (`addedCallers: [pool]`) → {@link setPool} → + * configure lanes. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @throws {@link CCTTxFailedError} if the tx reverts, fails, or mines without an address + * @example + * ```typescript + * const { hash, contractAddress, verification } = await cct.deployTokenPool({ + * type: 'LockReleaseTokenPool', + * token: '0xToken...', + * localTokenDecimals: 18, + * rmnProxy: '0xRmnProxy...', + * router: '0xRouter...', + * lockbox: '0xLockbox...', // required for LockReleaseTokenPool; must be a non-zero address + * wallet, + * }) + * ``` + */ + deployTokenPool(opts: EVMExecuteParams): Promise { + return this.#deployTokenPool.execute(this.chain, opts) + } + + /** + * Builds an unsigned `ERC20LockBox` (v2.0.0) deployment tx (for multisig / offline signing). + * A lockbox escrows a single `token` for `LockReleaseTokenPool`s. The deployed address is + * only known once mined, so it is NOT returned here — use {@link deployLockbox} to receive + * `{ hash, contractAddress, verification }`. + * @remarks Deploy the lockbox before its pool, then authorize the pool on it with + * {@link authorizeLockboxCallers} before the pool can lock/release. + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedDeployLockbox({ + * token: '0xToken...', // must be non-zero; the same token the LockReleaseTokenPool manages + * sender: '0xDeployer...', + * }) + * ``` + */ + generateUnsignedDeployLockbox(opts: DeployLockboxParams): Promise { + return this.#deployLockbox.generate(this.chain, opts) + } + + /** + * Deploys an `ERC20LockBox` (v2.0.0), signing + submitting with `opts.wallet`; resolves to the + * tx hash, the newly deployed lockbox address, and a `verification` + * ({@link ExplorerVerificationInput}) for verifying the source on a block explorer. + * @remarks Step two of the lock/release flow: {@link deployToken} → {@link deployLockbox} → + * {@link deployTokenPool} (passing this lockbox) → {@link authorizeLockboxCallers} + * (`addedCallers: [pool]`) → {@link setPool} → configure lanes. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @throws {@link CCTTxFailedError} if the tx reverts, fails, or mines without an address + * @example + * ```typescript + * const { hash, contractAddress, verification } = await cct.deployLockbox({ + * token: '0xToken...', + * wallet, + * }) + * ``` + */ + deployLockbox(opts: EVMExecuteParams): Promise { + return this.#deployLockbox.execute(this.chain, opts) + } + + /** + * Builds an unsigned `ERC20LockBox` `applyAuthorizedCallerUpdates` tx (for multisig / offline + * signing) that adds/removes authorized callers. Authorize a `LockReleaseTokenPool` here so it + * can lock/release against the lockbox. + * @throws {@link CCTParamsInvalidError} if any param is invalid, or if no caller is supplied + * @example + * ```typescript + * // `sender` must be the lockbox owner + * const unsigned = await cct.generateUnsignedAuthorizeLockboxCallers({ + * lockbox: '0xLockbox...', + * addedCallers: ['0xPool...'], // the LockReleaseTokenPool to authorize + * sender: '0xLockboxOwner...', + * }) + * ``` + */ + generateUnsignedAuthorizeLockboxCallers( + opts: AuthorizeLockboxCallersParams, + ): Promise { + return this.#authorizeLockboxCallers.generate(this.chain, opts) + } + + /** + * Adds/removes authorized callers on an `ERC20LockBox`, signing + submitting with `opts.wallet` + * (the lockbox owner). Authorize the `LockReleaseTokenPool` before it can lock/release. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid, or if no caller is supplied + * @throws {@link CCTTxFailedError} if the tx reverts or fails + * @example + * ```typescript + * // `wallet` must sign as the lockbox owner + * const { hash } = await cct.authorizeLockboxCallers({ + * lockbox: '0xLockbox...', + * addedCallers: ['0xPool...'], + * wallet, + * }) + * ``` + */ + authorizeLockboxCallers( + opts: EVMExecuteParams, + ): Promise { + return this.#authorizeLockboxCallers.execute(this.chain, opts) + } + + /** + * Reads a pool's admin state, v1.5.0 through v2.0.0: the `owner` every pool write is gated on, + * the `rateLimitAdmin` role, its token/router and configured lanes — plus, on v2.0.0 pools, the + * `feeAdmin` role, the allowed finality window, and a lock/release pool's `lockBox`. + * @remarks The result is a union: `state.version === '2.0.0'` gates the roles and finality + * window that version added, and `state.type === 'LockReleaseTokenPool'` gates its `lockBox` + * (see the example). A v2.0.0 `SiloedLockReleaseTokenPool` is rejected — it escrows per remote + * chain (`getLockBox(uint64)`). For a legacy pool's `allowList` / `rebalancer`, proxy/USDC + * pools, or v1.5.0 `*AndProxy` pools, use `cct.chain.getTokenPoolConfig()`, the tolerant + * transfer-flow read. No pool version exposes a pending-owner getter, so a proposed owner is + * not readable here. + * @remarks The Solana counterpart, `SolanaTokenManager.getTokenPoolState`, returns a different + * shape: its fields nest under `state.config` where these are flat, it spells `token` / + * `tokenDecimals` / `rmnProxy` as `config.mint` / `config.decimals` / `config.rmnRemote`, and its + * `version` is the account-layout number, not this protocol semver. `owner`, `rateLimitAdmin` + * and `router` are named alike on both. + * @throws {@link CCTParamsInvalidError} if `poolAddress` is not a valid address + * @throws {@link CCTContractTypeInvalidError} if the pool is not a supported CCT pool type + * @throws {@link CCTContractVersionUnsupportedError} if the pool's version is not a known one + * @example + * ```typescript + * const state = await cct.getTokenPoolState({ poolAddress: '0xPool...' }) + * // state.owner must sign transferOwnership / lane config; state.rateLimitAdmin may set rate limits + * if (state.version === '2.0.0') { + * console.log(state.feeAdmin, state.finalityDepth) + * if (state.type === 'LockReleaseTokenPool') console.log(state.lockBox) + * } + * ``` + */ + getTokenPoolState(opts: GetTokenPoolStateParams): Promise { + return this.#getTokenPoolState.query(this.chain, opts) + } +} + +export * from '../errors.ts' +export type { AcceptAdminParams } from './token-admin-registry/operations/accept-admin.ts' +export type { + RegisterAdminMethod, + RegisterAdminParams, +} from './token-admin-registry/operations/register-admin.ts' +export type { + GetTokenAdminRegistryParams, + GetTokenAdminRegistryResult, +} from './token-admin-registry/operations/get-token-admin-registry.ts' +export type { SetPoolParams } from './token-admin-registry/operations/set-pool.ts' +export type { TransferAdminParams } from './token-admin-registry/operations/transfer-admin.ts' +export type { + GetSupportedTokensParams, + GetSupportedTokensResult, +} from './token-admin-registry/operations/get-supported-tokens.ts' +export type { DeployTokenParams } from './token/operations/deploy-token.ts' +export type { + DeployTokenPoolParams, + DeployableTokenPoolType, +} from './token-pool/operations/deploy-token-pool.ts' +export type { + BurnMintTokenPoolStateV2_0_0, + GetTokenPoolStateParams, + GetTokenPoolStateResult, + LegacyTokenPoolState, + LockReleaseTokenPoolStateV2_0_0, + TokenPoolStateV2_0_0, +} from './token-pool/operations/get-token-pool-state.ts' +export type { DeployLockboxParams } from './lockbox/operations/deploy-lockbox.ts' +export type { AuthorizeLockboxCallersParams } from './lockbox/operations/authorize-callers.ts' +export type { + DeployArtifact, + DeployResult, + EVMExecuteParams, + ExplorerVerificationInput, +} from './operation.ts' +export type { TransactionResult } from '../operation.ts' diff --git a/ccip-sdk/src/cct/evm/lockbox/contracts.ts b/ccip-sdk/src/cct/evm/lockbox/contracts.ts new file mode 100644 index 00000000..afbe7086 --- /dev/null +++ b/ccip-sdk/src/cct/evm/lockbox/contracts.ts @@ -0,0 +1,29 @@ +/** + * EVM lockbox contract layer for CCT: the cached `ERC20LockBox` {@link Interface} + * ({@link LOCKBOX_INTERFACE}) for calldata encoding, and its deploy artifact + * ({@link getLockboxArtifact}). Only one lockbox version is deployable, so there is no version + * framework here. Mirrors `token/contracts.ts`. + * + * @packageDocumentation + */ + +import { Interface } from 'ethers' + +import ERC20_LOCKBOX_V2_0_0_ABI from '../artifacts/abi/V2_0_0/erc20-lockbox.ts' +import ERC20_LOCKBOX_V2_0_0_BYTECODE from '../artifacts/bytecode/V2_0_0/erc20-lockbox.ts' +import type { DeployArtifact } from '../operation.ts' + +/** Shared, cached `ERC20LockBox` interface for constructor and calldata encoding. */ +export const LOCKBOX_INTERFACE = new Interface(ERC20_LOCKBOX_V2_0_0_ABI) + +/** `ERC20LockBox` creation bytecode for `deployLockbox`. */ +export const LOCKBOX_BYTECODE = ERC20_LOCKBOX_V2_0_0_BYTECODE + +/** `ERC20LockBox` deploy artifact: contract name + ctor {@link Interface} + creation bytecode. */ +export function getLockboxArtifact(): DeployArtifact { + return { + contract: 'ERC20LockBox', + iface: LOCKBOX_INTERFACE, + bytecode: LOCKBOX_BYTECODE, + } +} diff --git a/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.test.ts b/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.test.ts new file mode 100644 index 00000000..72b7a585 --- /dev/null +++ b/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.test.ts @@ -0,0 +1,265 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ZeroAddress, getIcapAddress, makeError } from 'ethers' + +import { AuthorizeLockboxCallers } from './authorize-callers.ts' +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const SENDER = '0x' + '11'.repeat(20) +const LOCKBOX = '0x' + '66'.repeat(20) +const POOL = '0x' + '77'.repeat(20) +const OTHER = '0x' + '88'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +// applyAuthorizedCallerUpdates selector, per the vendored ABI (spec-pinned). +const SELECTOR = '0x91a2749a' +// Golden vectors: full literal calldata, hand-encoded from the ABI layout of +// applyAuthorizedCallerUpdates((address[] addedCallers, address[] removedCallers)) — a dynamic +// tuple of two dynamic address[] arrays. Pinning the whole byte string (rather than re-encoding +// through the SDK's own ABI) anchors every caller's position, so an added/removed swap or an +// ABI-ordering regression is caught instead of being mirrored into the expectation. +const W_TUPLE = '0000000000000000000000000000000000000000000000000000000000000020' // -> tuple +const OFF_40 = '0000000000000000000000000000000000000000000000000000000000000040' +const OFF_60 = '0000000000000000000000000000000000000000000000000000000000000060' +const OFF_80 = '0000000000000000000000000000000000000000000000000000000000000080' +const LEN_0 = '0000000000000000000000000000000000000000000000000000000000000000' +const LEN_1 = '0000000000000000000000000000000000000000000000000000000000000001' +// 20-byte address left-padded to a 32-byte word. +const word = (addr: string) => '000000000000000000000000' + addr.slice(2) + +/** Minimal EVMChain stub — the build path ignores it; execute uses only these. */ +function stubChain(): EVMChain { + return { + provider: {} as never, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +/** Fake ethers Signer for a plain (non-deployment) tx. */ +function fakeSigner(opts: { waitError?: Error } = {}) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(SENDER), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => + opts.waitError + ? Promise.reject(opts.waitError) + : Promise.resolve({ status: 1, contractAddress: null }), + }), + } +} + +describe('AuthorizeLockboxCallers (cct/evm lockbox operation)', () => { + describe('generate (golden vectors)', () => { + it('encodes an added caller as a call to the lockbox', async () => { + const unsigned = await new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: LOCKBOX, + addedCallers: [POOL], + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal(tx.to, LOCKBOX) + assert.equal(tx.from, SENDER) + assert.ok( + tx.data!.startsWith(SELECTOR), + 'data carries the applyAuthorizedCallerUpdates selector', + ) + // addedCallers:[POOL], removedCallers:[] — added array holds POOL, removed is empty. + assert.equal(tx.data, SELECTOR + W_TUPLE + OFF_40 + OFF_80 + LEN_1 + word(POOL) + LEN_0) + }) + + it('encodes both added and removed callers', async () => { + const unsigned = await new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: LOCKBOX, + addedCallers: [POOL], + removedCallers: [OTHER], + }) + // POOL sits in the added array, OTHER in the removed array — swapping them changes these bytes. + assert.equal( + unsigned.transactions[0]!.data, + SELECTOR + W_TUPLE + OFF_40 + OFF_80 + LEN_1 + word(POOL) + LEN_1 + word(OTHER), + ) + }) + + it('defaults omitted caller arrays to empty', async () => { + const unsigned = await new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: LOCKBOX, + removedCallers: [OTHER], + }) + // addedCallers omitted -> empty; OTHER lands in the removed array (removed offset is 0x60). + assert.equal( + unsigned.transactions[0]!.data, + SELECTOR + W_TUPLE + OFF_40 + OFF_60 + LEN_0 + LEN_1 + word(OTHER), + ) + }) + + it('omits `from` when no sender is given', async () => { + const unsigned = await new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: LOCKBOX, + addedCallers: [POOL], + }) + assert.equal(unsigned.transactions[0]!.from, undefined) + }) + }) + + describe('validation', () => { + it('rejects an invalid lockbox address', async () => { + await assert.rejects( + () => + new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: 'nope', + addedCallers: [POOL], + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'authorizeLockboxCallers' && + err.context.param === 'lockbox', + ) + }) + + it('rejects a zero-address lockbox', async () => { + // a call to 0x0 hits no code, so it would mine as a successful no-op + await assert.rejects( + () => + new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: ZeroAddress, + addedCallers: [POOL], + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'authorizeLockboxCallers' && + err.context.param === 'lockbox', + ) + }) + + it('rejects the zero address written in ICAP form', async () => { + // isAddress() accepts ICAP, and this never equals ZeroAddress literally + await assert.rejects( + () => + new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: getIcapAddress(ZeroAddress), + addedCallers: [POOL], + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'lockbox', + ) + }) + + it('rejects when no callers are supplied', async () => { + await assert.rejects( + () => new AuthorizeLockboxCallers().generate(stubChain(), { lockbox: LOCKBOX }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'addedCallers', + ) + }) + + it('rejects when both caller arrays are empty', async () => { + await assert.rejects( + () => + new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: LOCKBOX, + addedCallers: [], + removedCallers: [], + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'addedCallers', + ) + }) + + it('rejects an invalid added caller address', async () => { + await assert.rejects( + () => + new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: LOCKBOX, + addedCallers: [POOL, 'nope'], + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'addedCallers[1]', + ) + }) + + it('rejects an invalid removed caller address', async () => { + await assert.rejects( + () => + new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: LOCKBOX, + removedCallers: ['nope'], + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'removedCallers[0]', + ) + }) + + it('rejects the zero address as a caller', async () => { + await assert.rejects( + () => + new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: LOCKBOX, + addedCallers: [ZeroAddress], + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'addedCallers[0]', + ) + }) + + it('rejects an invalid sender', async () => { + await assert.rejects( + () => + new AuthorizeLockboxCallers().generate(stubChain(), { + lockbox: LOCKBOX, + addedCallers: [POOL], + sender: 'nope', + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await new AuthorizeLockboxCallers().execute(stubChain(), { + lockbox: LOCKBOX, + addedCallers: [POOL], + wallet: fakeSigner(), + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('throws CCIPExecTxRevertedError when the tx reverts on-chain', async () => { + await assert.rejects( + () => + new AuthorizeLockboxCallers().execute(stubChain(), { + lockbox: LOCKBOX, + addedCallers: [POOL], + wallet: fakeSigner({ waitError: makeError('execution reverted', 'CALL_EXCEPTION') }), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && + err.context.operation === 'authorizeLockboxCallers', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => + new AuthorizeLockboxCallers().execute(stubChain(), { + lockbox: LOCKBOX, + addedCallers: [POOL], + wallet: {}, + }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts b/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts new file mode 100644 index 00000000..e2650fc6 --- /dev/null +++ b/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts @@ -0,0 +1,67 @@ +/** + * authorizeLockboxCallers — adds/removes authorized callers on an `ERC20LockBox` (v2.0.0) via + * `applyAuthorizedCallerUpdates`. A `LockReleaseTokenPool` must be an authorized caller of its + * lockbox before it can lock/release. Mirrors `token-pool/operations/transfer-ownership.ts`. + * + * @packageDocumentation + */ + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation, callTx } from '../../operation.ts' +import { validateNonZeroAddress } from '../../validate.ts' +import { LOCKBOX_INTERFACE } from '../contracts.ts' + +/** + * Parameters for {@link AuthorizeLockboxCallers}. At least one caller across both arrays is required. + * @remarks `AuthorizedCallers._applyAuthorizedCallerUpdates` applies `removedCallers` first, so an + * address in both arrays ends up authorized. The list is a set: re-adding an existing caller is a + * no-op (though `AuthorizedCallerAdded` still fires), and removing an absent one emits nothing. + */ +export interface AuthorizeLockboxCallersParams { + /** Address of the `ERC20LockBox` to update. */ + lockbox: string + /** Callers to authorize (e.g. the `LockReleaseTokenPool`); defaults to `[]`. */ + addedCallers?: string[] + /** Callers to deauthorize; defaults to `[]`. */ + removedCallers?: string[] + /** Lockbox owner; sets `tx.from` for offline / multisig signing. */ + sender?: string +} + +/** Applies authorized-caller updates on an `ERC20LockBox` via `applyAuthorizedCallerUpdates`. */ +export class AuthorizeLockboxCallers extends EVMOperation { + readonly name = 'authorizeLockboxCallers' + + /** Validates the lockbox and every caller address; requires at least one caller. */ + protected validate({ + lockbox, + addedCallers = [], + removedCallers = [], + }: AuthorizeLockboxCallersParams): void { + validateNonZeroAddress(this.name, 'lockbox', lockbox) + if (addedCallers.length + removedCallers.length === 0) { + throw new CCTParamsInvalidError( + this.name, + 'addedCallers', + 'at least one caller must be added or removed', + ) + } + const validateCaller = (field: string, c: string, i: number): void => + validateNonZeroAddress(this.name, `${field}[${i}]`, c) + addedCallers.forEach((c, i) => validateCaller('addedCallers', c, i)) + removedCallers.forEach((c, i) => validateCaller('removedCallers', c, i)) + } + + /** Builds `applyAuthorizedCallerUpdates` calldata targeting the lockbox. */ + protected buildUnsigned( + _chain: EVMChain, + { lockbox, addedCallers = [], removedCallers = [] }: AuthorizeLockboxCallersParams, + ): UnsignedEVMTx { + const data = LOCKBOX_INTERFACE.encodeFunctionData('applyAuthorizedCallerUpdates', [ + { addedCallers, removedCallers }, + ]) + return callTx(lockbox, data) + } +} diff --git a/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.test.ts b/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.test.ts new file mode 100644 index 00000000..d124063b --- /dev/null +++ b/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.test.ts @@ -0,0 +1,152 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import { DeployLockbox } from './deploy-lockbox.ts' +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import ERC20_LOCKBOX_V2_0_0_ABI from '../../artifacts/abi/V2_0_0/erc20-lockbox.ts' +import LOCKBOX_V2_0_0 from '../../artifacts/bytecode/V2_0_0/erc20-lockbox.ts' + +const SENDER = '0x' + '11'.repeat(20) +const TOKEN = '0x' + '22'.repeat(20) +const DEPLOYED = '0x' + '77'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +// Golden vector: the ctor arg is a single 32-byte word holding the token address. Computed +// independently with a fresh ethers Interface so it guards the SDK's init-code against drift. +const W_TOKEN = '0000000000000000000000002222222222222222222222222222222222222222' +const CTOR_ARGS = new Interface(ERC20_LOCKBOX_V2_0_0_ABI).encodeDeploy([TOKEN]) + +/** Minimal EVMChain stub — deployLockbox's build path ignores it; execute uses only these. */ +function stubChain(): EVMChain { + return { + provider: {} as never, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +/** Fake ethers Signer whose deployment receipt carries `contractAddress`. */ +function fakeSigner(opts: { contractAddress?: string | null; waitError?: Error }) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(SENDER), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => + opts.waitError + ? Promise.reject(opts.waitError) + : Promise.resolve({ + status: 1, + contractAddress: + opts.contractAddress === undefined ? DEPLOYED : opts.contractAddress, + }), + }), + } +} + +describe('DeployLockbox (cct/evm lockbox operation)', () => { + describe('generate (golden vector)', () => { + it('builds the lockbox as init-code with no `to`', async () => { + const unsigned = await new DeployLockbox().generate(stubChain(), { + token: TOKEN, + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal(tx.to, undefined, 'deployment tx has no `to`') + assert.equal(tx.from, SENDER) + assert.ok(tx.data!.startsWith(LOCKBOX_V2_0_0), 'data starts with creation bytecode') + // Pinned bytes: the constructor arg is exactly the token address, left-padded to 32 bytes. + assert.equal(CTOR_ARGS, '0x' + W_TOKEN) + assert.equal(tx.data, LOCKBOX_V2_0_0 + W_TOKEN) + }) + + it('omits `from` when no sender is given', async () => { + const unsigned = await new DeployLockbox().generate(stubChain(), { token: TOKEN }) + assert.equal(unsigned.transactions[0]!.from, undefined) + }) + }) + + describe('validation', () => { + it('rejects an invalid token address', async () => { + await assert.rejects( + () => new DeployLockbox().generate(stubChain(), { token: 'nope' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'deployLockbox' && + err.context.param === 'token', + ) + }) + + it('rejects the zero address for token', async () => { + await assert.rejects( + () => new DeployLockbox().generate(stubChain(), { token: ZeroAddress }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'token', + ) + }) + + it('rejects an invalid sender', async () => { + await assert.rejects( + () => new DeployLockbox().generate(stubChain(), { token: TOKEN, sender: 'nope' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + }) + + describe('execute', () => { + it('deploys and returns the tx hash and deployed address', async () => { + const result = await new DeployLockbox().execute(stubChain(), { + token: TOKEN, + wallet: fakeSigner({ contractAddress: DEPLOYED }), + }) + assert.deepEqual(result, { + hash: HASH, + contractAddress: DEPLOYED, + verification: { contract: 'ERC20LockBox', encodedConstructorArgs: '0x' + W_TOKEN }, + }) + }) + + it('throws CCTTxFailedError when the receipt carries no contract address', async () => { + await assert.rejects( + () => + new DeployLockbox().execute(stubChain(), { + token: TOKEN, + wallet: fakeSigner({ contractAddress: null }), + }), + (err: unknown) => + err instanceof CCTTxFailedError && + err.context.operation === 'deployLockbox' && + !err.isTransient, + ) + }) + + it('throws CCIPExecTxRevertedError when the deployment reverts on-chain', async () => { + await assert.rejects( + () => + new DeployLockbox().execute(stubChain(), { + token: TOKEN, + wallet: fakeSigner({ waitError: makeError('execution reverted', 'CALL_EXCEPTION') }), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && err.context.operation === 'deployLockbox', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => new DeployLockbox().execute(stubChain(), { token: TOKEN, wallet: {} }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts b/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts new file mode 100644 index 00000000..70309fe4 --- /dev/null +++ b/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts @@ -0,0 +1,42 @@ +/** + * deployLockbox — deploys an `ERC20LockBox` (v2.0.0) via raw init-code. The tx has no + * `to`; `execute` returns the deployed contract address. A lockbox escrows a single token + * for `LockReleaseTokenPool`s; deploy it before the pool, then authorize the pool on it via + * {@link AuthorizeLockboxCallers}. Mirrors `token-pool/operations/deploy-token-pool.ts`. + * + * @packageDocumentation + */ + +import type { Interface } from 'ethers' + +import { type DeployArtifact, EVMDeployOperation } from '../../operation.ts' +import { validateNonZeroAddress } from '../../validate.ts' +import { getLockboxArtifact } from '../contracts.ts' + +/** Parameters for {@link DeployLockbox} — deploys `ERC20LockBox` (v2.0.0). */ +export interface DeployLockboxParams { + /** Address of the token the lockbox escrows; the v2.0.0 constructor reverts on the zero address. */ + token: string + /** Deployer address; sets `tx.from` for offline / multisig signing. */ + sender?: string +} + +/** Deploys an `ERC20LockBox`; `execute` resolves to `{ hash, contractAddress, verification }`. */ +export class DeployLockbox extends EVMDeployOperation { + readonly name = 'deployLockbox' + + /** Validates the constructor params before building init-code. */ + protected validate(params: DeployLockboxParams): void { + validateNonZeroAddress(this.name, 'token', params.token) + } + + /** Deploy artifact for `ERC20LockBox` (v2.0.0). */ + protected artifact(): DeployArtifact { + return getLockboxArtifact() + } + + /** ABI-encodes the `ERC20LockBox` (v2.0.0) constructor args. */ + protected encode(iface: Interface, p: DeployLockboxParams): string { + return iface.encodeDeploy([p.token]) + } +} diff --git a/ccip-sdk/src/cct/evm/operation.ts b/ccip-sdk/src/cct/evm/operation.ts new file mode 100644 index 00000000..8928da20 --- /dev/null +++ b/ccip-sdk/src/cct/evm/operation.ts @@ -0,0 +1,189 @@ +/** + * EVM {@link Operation} lifecycle: validate → encode → submit. + * Concrete ops implement {@link EVMOperation.buildUnsigned}; the base wires + * {@link generate} and {@link execute}. Deployment ops instead extend + * {@link EVMDeployOperation}, supplying a {@link DeployArtifact} and constructor-arg + * encoding while inheriting a deploy-aware {@link execute} that also returns the + * deployed address, reusing {@link submit}. + * + * @packageDocumentation + */ + +import { type Interface, getAddress } from 'ethers' + +import { CCIPWalletInvalidError } from '../../errors/index.ts' +import { type EVMChain, isSigner } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import { ChainFamily } from '../../networks.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../errors.ts' +import { type ExecuteParams, type TransactionResult, Operation } from '../operation.ts' +import { submit } from './submit.ts' +import { validateAddress } from './validate.ts' + +/** Assembles a contract-deployment tx (no `to`): creation bytecode + ABI-encoded ctor args. */ +export function deployTx(bytecode: `0x${string}`, ctorArgs: string): UnsignedEVMTx { + return { family: ChainFamily.EVM, transactions: [{ data: bytecode + ctorArgs.slice(2) }] } +} + +/** Assembles an unsigned call to an existing contract: `to` + ABI-encoded calldata. */ +export function callTx(to: string, data: string): UnsignedEVMTx { + return { family: ChainFamily.EVM, transactions: [{ to, data }] } +} + +/** + * The deploy-side inputs a block explorer needs to verify a contract's source: its name and + * ABI-encoded constructor args, captured while deploying with no extra RPC. + * @remarks A constructor-args companion, *not* proof of verification — nothing here is read back + * from the chain or submitted anywhere. A full submission also needs the source/compiler side + * (standard-json input plus the matching solc version and settings), which this SDK does not + * vendor; those ship in the `@chainlink/contracts-ccip` package. + * + * Only available from `execute`, which deploys and so learns the address. The + * `generateUnsigned*` builders return the unsigned tx alone. + * @example Verifying on Etherscan, whose "Constructor Arguments" field wants the args bare: + * ```typescript + * const { contractAddress, verification } = await cct.deployTokenPool({ ...params, wallet }) + * console.log(verification.contract) // 'LockReleaseTokenPool' + * console.log(verification.encodedConstructorArgs.slice(2)) // drop the `0x` + * ``` + */ +export interface ExplorerVerificationInput { + /** Contract name as compiled, e.g. `BurnMintTokenPool`; unqualified, matching the artifact. */ + contract: string + /** 0x-prefixed ABI-encoded constructor args, or just `0x` when the constructor takes none. */ + encodedConstructorArgs: string +} + +/** + * A contract deploy artifact: the contract name (for verification), the cached constructor + * {@link Interface}, and the creation bytecode. Field is `iface` (not `interface`, a reserved word). + */ +export interface DeployArtifact { + contract: string + iface: Interface + bytecode: `0x${string}` +} + +/** EVM {@link ExecuteParams} — EVM ops need nothing beyond the signing `wallet`. */ +export type EVMExecuteParams

= ExecuteParams

+ +/** + * Result of a successful EVM deployment write: the tx hash plus the deployed + * contract address (token, pool, etc.). Also carries the + * {@link ExplorerVerificationInput} needed to verify the contract's source on a + * block explorer — additive, so readers of `{ hash, contractAddress }` are unaffected. + */ +export type DeployResult = TransactionResult & { + contractAddress: string + verification: ExplorerVerificationInput +} + +/** + * EVM CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}; + * {@link execute} signs and submits, returning the confirmed tx hash. Ops that + * resolve to more (e.g. a deployed address) extend {@link EVMDeployOperation}. + */ +export abstract class EVMOperation

extends Operation< + EVMChain, + P, + UnsignedEVMTx, + TransactionResult +> { + /** Build calldata into an unsigned tx; versioned ops resolve their encoder here. */ + protected abstract buildUnsigned( + chain: EVMChain, + params: P, + ): Promise | UnsignedEVMTx + + /** Run {@link validate} and {@link buildUnsigned}, applying optional `sender`; no signing. */ + async generate(chain: EVMChain, params: P): Promise { + this.validate(params) + if (params.sender !== undefined) validateAddress(this.name, 'sender', params.sender) + const unsigned = await this.buildUnsigned(chain, params) + if (params.sender && unsigned.transactions[0]) unsigned.transactions[0].from = params.sender + return unsigned + } + + /** + * Resolves the address a signed submission is authorized against: the signing wallet's own. + * The chain gates on `msg.sender`, and {@link submit} clears any builder-set `tx.from` before + * populating the tx (so ethers' own from/signer guard never fires) — an explicit `sender` that + * differs from the wallet would therefore let an op's pre-tx checks authorize one address while + * a different one actually signs, passing every local guard and reverting on-chain. Ops that + * gate on an on-chain role call this from `execute`; build with `generateUnsigned*` instead + * when the eventual signer isn't known yet, where `sender` is trusted as given. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if `sender` is given and is not the wallet's address + */ + protected async senderBoundToWallet(wallet: unknown, sender?: string): Promise { + if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) + const walletAddress = await wallet.getAddress() + if (sender === undefined) return walletAddress + // Validated before `getAddress`, which throws a raw ethers TypeError on a malformed string. + // This runs ahead of `generate`'s own validate(), so without it the documented + // CCTParamsInvalidError contract would leak an ethers error for a bad `sender`. + validateAddress(this.name, 'sender', sender) + if (getAddress(sender) !== getAddress(walletAddress)) + throw new CCTParamsInvalidError( + this.name, + 'sender', + `must be the executing wallet address (${walletAddress}) — use generateUnsigned${this.name[0]!.toUpperCase()}${this.name.slice(1)} for externally-signed transactions`, + ) + return sender + } + + /** {@link generate}, then sign and submit; returns the confirmed tx hash. */ + async execute(chain: EVMChain, params: EVMExecuteParams

): Promise { + const { response } = await submit( + chain, + params.wallet, + await this.generate(chain, params), + this.name, + ) + return { hash: response.hash } + } +} + +/** + * EVM contract-deployment base. Subclasses supply {@link validate}, {@link artifact} (name + + * ctor {@link Interface} + creation bytecode), and {@link encode}; the base wires + * {@link buildUnsigned} (init-code = bytecode + encoded ctor args) and {@link execute} (submit, + * then read the deployed address and pair it with an {@link ExplorerVerificationInput}). + */ +export abstract class EVMDeployOperation

extends EVMOperation

{ + /** Contract name, ctor {@link Interface}, and creation bytecode for this deployment. */ + protected abstract artifact(params: P): DeployArtifact + + /** ABI-encodes the constructor args (0x-prefixed) for this deployment. */ + protected abstract encode(iface: Interface, params: P): string + + /** Builds a deployment tx (no `to`): creation bytecode + ABI-encoded constructor args. */ + protected buildUnsigned(_chain: EVMChain, params: P): UnsignedEVMTx { + const a = this.artifact(params) + return deployTx(a.bytecode, this.encode(a.iface, params)) + } + + /** + * {@link generate}, then sign and submit; resolves to the tx hash and the newly deployed + * contract address (read from the mined receipt), plus the + * {@link ExplorerVerificationInput} for verifying its source on a block explorer. + * @throws {@link CCTTxFailedError} if the tx mined without producing a contract address + */ + override async execute(chain: EVMChain, params: EVMExecuteParams

): Promise { + const unsigned = await this.generate(chain, params) + const { contract, iface } = this.artifact(params) + // Same value `buildUnsigned` appended to the bytecode. Taken from `encode` rather than + // sliced back out of the init-code, so it stays correct regardless of the tx layout. + const encodedConstructorArgs = this.encode(iface, params) + const { response, receipt } = await submit(chain, params.wallet, unsigned, this.name) + if (!receipt.contractAddress) + throw new CCTTxFailedError(this.name, 'deployment produced no contract address', { + context: { txHash: response.hash }, + }) + return { + hash: response.hash, + contractAddress: receipt.contractAddress, + verification: { contract, encodedConstructorArgs }, + } + } +} diff --git a/ccip-sdk/src/cct/evm/query.ts b/ccip-sdk/src/cct/evm/query.ts new file mode 100644 index 00000000..8406b9eb --- /dev/null +++ b/ccip-sdk/src/cct/evm/query.ts @@ -0,0 +1,35 @@ +/** + * EVM CCT reads: {@link Query} bound to an {@link EVMChain}, plus {@link getTypedContract}, the + * call-typed handle read ops decode through. Mirrors `cct/solana/query.ts`. + * + * @packageDocumentation + */ + +import type { Abi } from 'abitype' +import { type InterfaceAbi, Contract } from 'ethers' +import type { TypedContract } from 'ethers-abitype' + +import type { EVMChain } from '../../evm/index.ts' +import { Query } from '../query.ts' + +/** Shared base for read-only EVM CCT queries; see {@link Query}. */ +export abstract class EVMQuery

extends Query< + EVMChain, + P, + R, + Parsed +> {} + +/** + * Binds `address` to `abi` as a call-typed contract for read ops: one value both types the calls + * and builds the runtime `Interface`. + * @remarks The CCT layer's single ethers → `ethers-abitype` cast; the library's own + * `typedContract` would avoid it, but its ESM entry is unusable (`main` resolves to CJS). + */ +export function getTypedContract( + chain: EVMChain, + address: string, + abi: ABI & InterfaceAbi, +): TypedContract { + return new Contract(address, abi, chain.provider) as unknown as TypedContract +} diff --git a/ccip-sdk/src/cct/evm/submit.test.ts b/ccip-sdk/src/cct/evm/submit.test.ts new file mode 100644 index 00000000..1303341a --- /dev/null +++ b/ccip-sdk/src/cct/evm/submit.test.ts @@ -0,0 +1,142 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { makeError } from 'ethers' + +import { submit } from './submit.ts' +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../errors/index.ts' +import type { EVMChain } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import { ChainFamily } from '../../networks.ts' +import { CCTTxFailedError, CCTTxNotConfirmedError } from '../errors.ts' + +const TAR = '0x' + '44'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +const UNSIGNED: UnsignedEVMTx = { + family: ChainFamily.EVM, + transactions: [{ to: TAR, data: '0x1234' }], +} + +function stubChain(): EVMChain { + return { + provider: {} as never, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +/** + * Fake ethers Signer. `wait` resolves to `receipt` (or rejects with `waitError`); + * `submitError` makes both send and sign paths reject (pre-broadcast failure). + */ +function fakeSigner(opts: { + receipt?: { status: number; contractAddress?: string | null } | null + waitError?: Error + submitError?: Error +}) { + const fail = opts.submitError + return { + signTransaction: () => (fail ? Promise.reject(fail) : Promise.resolve('0x')), + getAddress: () => Promise.resolve('0x' + '55'.repeat(20)), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: (_tx: unknown) => + fail + ? Promise.reject(fail) + : Promise.resolve({ + hash: HASH, + wait: (_c?: number, _t?: number) => + opts.waitError + ? Promise.reject(opts.waitError) + : Promise.resolve(opts.receipt ?? null), + }), + } +} + +describe('submit (sign-and-confirm pipeline)', () => { + it('returns the broadcast response and mined receipt', async () => { + const { response, receipt } = await submit( + stubChain(), + fakeSigner({ receipt: { status: 1, contractAddress: null } }), + UNSIGNED, + 'setPool', + ) + assert.equal(response.hash, HASH) + assert.equal(receipt.status, 1) + }) + + it('throws CCIPExecTxRevertedError (non-transient) when wait() throws CALL_EXCEPTION', async () => { + await assert.rejects( + () => + submit( + stubChain(), + fakeSigner({ waitError: makeError('execution reverted', 'CALL_EXCEPTION') }), + UNSIGNED, + 'setPool', + ), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && + err.context.operation === 'setPool' && + err.context.txHash === HASH && + !err.isTransient && + err.message.includes('reverted'), + ) + }) + + it('throws CCTTxNotConfirmedError (transient) when wait() throws TRANSACTION_REPLACED', async () => { + await assert.rejects( + () => + submit( + stubChain(), + fakeSigner({ waitError: makeError('transaction replaced', 'TRANSACTION_REPLACED') }), + UNSIGNED, + 'setPool', + ), + (err: unknown) => + err instanceof CCTTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, + ) + }) + + it('throws CCTTxNotConfirmedError (transient, keeps hash) when no receipt arrives', async () => { + await assert.rejects( + () => submit(stubChain(), fakeSigner({ receipt: null }), UNSIGNED, 'setPool'), + (err: unknown) => + err instanceof CCTTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, + ) + }) + + it('throws CCTTxNotConfirmedError (transient, keeps hash) on confirmation timeout', async () => { + await assert.rejects( + () => + submit( + stubChain(), + fakeSigner({ waitError: makeError('timed out', 'TIMEOUT') }), + UNSIGNED, + 'setPool', + ), + (err: unknown) => + err instanceof CCTTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, + ) + }) + + it('throws a transient CCTTxFailedError when submission fails with a network error', async () => { + await assert.rejects( + () => + submit( + stubChain(), + fakeSigner({ submitError: makeError('network down', 'NETWORK_ERROR') }), + UNSIGNED, + 'setPool', + ), + (err: unknown) => err instanceof CCTTxFailedError && err.isTransient, + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => submit(stubChain(), {}, UNSIGNED, 'setPool'), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/submit.ts b/ccip-sdk/src/cct/evm/submit.ts new file mode 100644 index 00000000..45c5dc41 --- /dev/null +++ b/ccip-sdk/src/cct/evm/submit.ts @@ -0,0 +1,95 @@ +/** + * Shared sign-and-submit pipeline for EVM CCT operations. Maps broadcast and + * confirmation failures to {@link CCTTxFailedError} / {@link CCTTxNotConfirmedError}, + * and on-chain reverts to {@link CCIPExecTxRevertedError}. Operations map the + * confirmed `{ response, receipt }` to their own result shape. + * + * @packageDocumentation + */ + +import { + type TransactionReceipt, + type TransactionRequest, + type TransactionResponse, + isError, +} from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../errors/index.ts' +import { type EVMChain, isSigner, submitTransaction } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import { CCTTxFailedError, CCTTxNotConfirmedError } from '../errors.ts' + +/** Max ms to wait for one confirmation before throwing {@link CCTTxNotConfirmedError}. */ +const CONFIRM_TIMEOUT_MS = 60_000 + +/** True for ethers infra errors worth retrying (not an on-chain revert). */ +function isTransientError(error: unknown): boolean { + return ( + isError(error, 'TIMEOUT') || isError(error, 'NETWORK_ERROR') || isError(error, 'SERVER_ERROR') + ) +} + +/** + * Signs and submits the first transaction in `unsigned`, then waits for one confirmation. + * Returns the broadcast `response` and mined `receipt`; callers map these to their + * own result shape (see {@link EVMOperation.execute}). + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxNotConfirmedError} if broadcast but not confirmed in time + */ +export async function submit( + chain: EVMChain, + wallet: unknown, + unsigned: UnsignedEVMTx, + operation: string, +): Promise<{ response: TransactionResponse; receipt: TransactionReceipt }> { + if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) + const sender = await wallet.getAddress() + chain.logger.debug(`${operation}: submitting...`) + + const [first] = unsigned.transactions + if (!first) throw new CCTTxFailedError(operation, 'no transaction to submit') + + let response: TransactionResponse + let nonceConsumed = false + try { + let tx: TransactionRequest = { ...first } + tx.from = undefined // drop any builder-set sender before populate, else ethers throws on a from/signer mismatch + if (tx.nonce == null) { + tx.nonce = await chain.nextNonce(sender) + nonceConsumed = true + } + tx = await wallet.populateTransaction(tx) + tx.from = undefined // some signers reject a pre-populated `from` + response = await submitTransaction(wallet, tx, chain.provider) + } catch (error) { + if (nonceConsumed) chain.rollbackNonce(sender) + throw new CCTTxFailedError(operation, error instanceof Error ? error.message : String(error), { + cause: error instanceof Error ? error : undefined, + isTransient: isTransientError(error), + }) + } + + chain.logger.debug(`${operation}: waiting for confirmation, tx =`, response.hash) + + let receipt: TransactionReceipt | null + try { + receipt = await response.wait(1, CONFIRM_TIMEOUT_MS) + } catch (error) { + if (isError(error, 'CALL_EXCEPTION')) { + // mined revert — permanent; reuse the core revert error so consumers catch + // one type across core `execute` and CCT ops. + throw new CCIPExecTxRevertedError(response.hash, { cause: error, context: { operation } }) + } + // broadcast already succeeded; any non-revert error leaves the tx in an unknown state + throw new CCTTxNotConfirmedError(operation, response.hash, { + cause: error instanceof Error ? error : undefined, + }) + } + + if (!receipt) throw new CCTTxNotConfirmedError(operation, response.hash) + + chain.logger.info(`${operation}: confirmed, tx =`, response.hash) + return { response, receipt } +} diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/contracts.ts b/ccip-sdk/src/cct/evm/token-admin-registry/contracts.ts new file mode 100644 index 00000000..8893d5c8 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/contracts.ts @@ -0,0 +1,185 @@ +/** + * EVM token-admin-registry contract layer for CCT — the two contracts the admin ops talk to: + * + * - **`TokenAdminRegistry`** ({@link getTokenAdminRegistryInterface}) — holds each token's + * administrator/pool entry. Every admin write is gated on who currently holds those roles, so + * the ops also share one spelling of that read ({@link readTokenAdminRegistryConfig}, + * {@link isRegistryModule}) rather than each deriving a handle. + * - **`RegistryModuleOwnerCustom`** ({@link getRegistryModuleOwnerCustomInterface}) — the + * self-service module `registerAdmin` calls to propose an administrator without the registry + * owner's help. + * + * Neither is deployed by this SDK, so unlike `token/contracts.ts` and `token-pool/contracts.ts` + * there are no bytecode or {@link DeployArtifact} entries here — only interfaces and reads. + * Mirrors `lockbox/contracts.ts` in shape, `token/contracts.ts` in the version-keyed accessors. + * + * @packageDocumentation + */ + +import { Interface, getAddress } from 'ethers' +import type { TypedContract } from 'ethers-abitype' + +import type { EVMChain } from '../../../evm/index.ts' +import { resultToObject } from '../../../evm/types.ts' +import { CCTContractTypeInvalidError, CCTContractVersionUnsupportedError } from '../../errors.ts' +import REGISTRY_MODULE_OWNER_CUSTOM_V1_5_0_ABI from '../artifacts/abi/V1_5_0/registry-module-owner-custom.ts' +import TOKEN_ADMIN_REGISTRY_V1_5_0_ABI from '../artifacts/abi/V1_5_0/token-admin-registry.ts' +import REGISTRY_MODULE_OWNER_CUSTOM_V1_6_0_ABI from '../artifacts/abi/V1_6_0/registry-module-owner-custom.ts' +import { getTypedContract } from '../query.ts' + +/** + * Known `TokenAdminRegistry` versions. Only `1.5.0` is vendored: the admin surface this SDK uses + * (`getTokenConfig`, `isRegistryModule`, `proposeAdministrator`, `transferAdminRole`, + * `acceptAdminRole`, `setPool`) is byte-identical from v1.5 through v2.0, so one ABI serves them + * all and no version dispatch is needed. + */ +export const TokenAdminRegistryVersion = { + V1_5_0: '1.5.0', +} as const + +/** A known `TokenAdminRegistry` version. */ +export type TokenAdminRegistryVersion = + (typeof TokenAdminRegistryVersion)[keyof typeof TokenAdminRegistryVersion] + +/** + * Known `RegistryModuleOwnerCustom` versions, low to high. `1.6.0` added + * `registerAccessControlDefaultAdmin`; the two share `registerAdminViaOwner` and + * `registerAdminViaGetCCIPAdmin`. + */ +export const RegistryModuleOwnerCustomVersion = { + V1_5_0: '1.5.0', + V1_6_0: '1.6.0', +} as const + +/** A known `RegistryModuleOwnerCustom` version. */ +export type RegistryModuleOwnerCustomVersion = + (typeof RegistryModuleOwnerCustomVersion)[keyof typeof RegistryModuleOwnerCustomVersion] + +/** + * Cached `TokenAdminRegistry` {@link Interface}s per {@link TokenAdminRegistryVersion}, built once + * from the vendored ABI (no per-call `new Interface`). Mirrors `TOKEN_INTERFACES` in + * `token/contracts.ts`. + */ +export const TOKEN_ADMIN_REGISTRY_INTERFACES: Record = { + [TokenAdminRegistryVersion.V1_5_0]: new Interface(TOKEN_ADMIN_REGISTRY_V1_5_0_ABI), +} + +/** + * Cached `RegistryModuleOwnerCustom` {@link Interface}s per + * {@link RegistryModuleOwnerCustomVersion}, each built from its own vendored ABI. The shared + * functions encode identically at both versions, so the split is not about calldata — it is about + * *which functions exist*: only `1.6.0` knows `registerAccessControlDefaultAdmin`, so encoding it + * against the `1.5.0` interface throws instead of producing calldata a v1.5.0 module would reject. + */ +export const REGISTRY_MODULE_OWNER_CUSTOM_INTERFACES: Record< + RegistryModuleOwnerCustomVersion, + Interface +> = { + [RegistryModuleOwnerCustomVersion.V1_5_0]: new Interface(REGISTRY_MODULE_OWNER_CUSTOM_V1_5_0_ABI), + [RegistryModuleOwnerCustomVersion.V1_6_0]: new Interface(REGISTRY_MODULE_OWNER_CUSTOM_V1_6_0_ABI), +} + +/** Type guard for {@link RegistryModuleOwnerCustomVersion}. */ +export function isRegistryModuleOwnerCustomVersion( + v: string, +): v is RegistryModuleOwnerCustomVersion { + return Object.values(RegistryModuleOwnerCustomVersion).some((known) => known === v) +} + +/** `typeAndVersion` prefix every `RegistryModuleOwnerCustom` reports. */ +const REGISTRY_MODULE_OWNER_CUSTOM = 'RegistryModuleOwnerCustom' + +/** + * Resolves a deployed module's version from its `typeAndVersion`, narrowed to a known + * {@link RegistryModuleOwnerCustomVersion}. Mirrors `resolveTokenPool` in + * `token-pool/contracts.ts`. + * @throws {@link CCTContractTypeInvalidError} if `address` is not a `RegistryModuleOwnerCustom` + * @throws {@link CCTContractVersionUnsupportedError} if it reports an unknown version + */ +export async function resolveRegistryModuleOwnerCustom( + chain: EVMChain, + address: string, +): Promise { + const [contractType, version] = await chain.typeAndVersion(address) + if (contractType !== REGISTRY_MODULE_OWNER_CUSTOM) + throw new CCTContractTypeInvalidError(address, REGISTRY_MODULE_OWNER_CUSTOM, contractType) + if (!isRegistryModuleOwnerCustomVersion(version)) + throw new CCTContractVersionUnsupportedError(contractType, version, { context: { address } }) + return version +} + +/** Returns the cached `TokenAdminRegistry` {@link Interface} for `version`. */ +export function getTokenAdminRegistryInterface( + version: TokenAdminRegistryVersion = TokenAdminRegistryVersion.V1_5_0, +): Interface { + return TOKEN_ADMIN_REGISTRY_INTERFACES[version] +} + +/** Returns the cached `RegistryModuleOwnerCustom` {@link Interface} for `version`. */ +export function getRegistryModuleOwnerCustomInterface( + version: RegistryModuleOwnerCustomVersion = RegistryModuleOwnerCustomVersion.V1_6_0, +): Interface { + return REGISTRY_MODULE_OWNER_CUSTOM_INTERFACES[version] +} + +/** + * Call-typed TokenAdminRegistry handle bound to `registry` on `chain`'s provider, so the ops don't + * each re-derive one. Goes through {@link getTypedContract}, the CCT layer's single + * ethers → `ethers-abitype` cast. + */ +function tokenAdminRegistry( + chain: EVMChain, + registry: string, +): TypedContract { + return getTypedContract(chain, registry, TOKEN_ADMIN_REGISTRY_V1_5_0_ABI) +} + +/** + * A token's entry in the TokenAdminRegistry, checksummed, with zero addresses preserved rather + * than omitted — callers distinguish the registry's states by comparing against `ZeroAddress`: + * + * | `administrator` | `pendingAdministrator` | state | + * | --------------- | ---------------------- | ------------------------------------- | + * | zero | zero | not registered | + * | zero | set | registered, awaiting `acceptAdmin` | + * | set | zero | active admin | + * | set | set | active admin, `transferAdmin` pending | + */ +export type TokenAdminRegistryConfig = { + administrator: string + pendingAdministrator: string + tokenPool: string +} + +/** + * Reads a token's TAR entry through the vendored ABI. + * + * @remarks Deliberately **not** {@link EVMChain.getRegistryTokenConfig}: that helper throws + * `CCIPTokenNotConfiguredError` whenever `administrator` is the zero address, which is precisely + * the registered-but-not-yet-accepted state these ops must be able to observe and report. Reading + * `getTokenConfig` directly keeps every row of the table above reachable. + */ +export async function readTokenAdminRegistryConfig( + chain: EVMChain, + registry: string, + token: string, +): Promise { + const config = resultToObject(await tokenAdminRegistry(chain, registry).getTokenConfig(token)) + return { + administrator: getAddress(config.administrator), + pendingAdministrator: getAddress(config.pendingAdministrator), + tokenPool: getAddress(config.tokenPool), + } +} + +/** + * Whether the TAR recognises `module` as a registry module — the only on-chain question it can + * answer about one, since it exposes no way to enumerate them. + */ +export function isRegistryModule( + chain: EVMChain, + registry: string, + module: string, +): Promise { + return tokenAdminRegistry(chain, registry).isRegistryModule(module) +} diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin.test.ts new file mode 100644 index 00000000..3b6bf188 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin.test.ts @@ -0,0 +1,357 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ZeroAddress, getAddress, getIcapAddress, id, makeError } from 'ethers' + +import { AcceptAdmin } from './accept-admin.ts' +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +// SENDER and OTHER carry hex letters so their checksummed and lowercase spellings differ. That +// difference is what makes the `getAddress()` normalisation in the pending-admin and wallet-binding +// comparisons observable: with all-digit fixtures both spellings are identical, so dropping the +// normalisation would pass every test while locking a legitimate admin out in production (a +// lowercase address from an indexer vs a checksummed one decoded from the chain). +const SENDER = getAddress('0x' + 'ab'.repeat(20)) +const OTHER = getAddress('0x' + 'cd'.repeat(20)) +const TOKEN = '0x' + '33'.repeat(20) +const TAR = '0x' + '44'.repeat(20) +const ADDRESS = '0x' + '55'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +// acceptAdminRole(address) selector, per the vendored ABI (spec-pinned). +const SELECTOR = id('acceptAdminRole(address)').slice(0, 10) +// 20-byte address left-padded to a 32-byte word; lowercased, since ABI encoding emits lowercase hex +// regardless of how the caller spelled the address. +const word = (addr: string) => '000000000000000000000000' + addr.slice(2).toLowerCase() + +/** Encodes a `getTokenConfig` return value against the vendored TokenAdminRegistry ABI. */ +function encodeTokenConfig(config: { + administrator?: string + pendingAdministrator?: string + tokenPool?: string +}): string { + return interfaces.TokenAdminRegistry.encodeFunctionResult('getTokenConfig', [ + [ + config.administrator ?? ZeroAddress, + config.pendingAdministrator ?? ZeroAddress, + config.tokenPool ?? ZeroAddress, + ], + ]) +} + +/** + * Fake provider whose `call` answers `getTokenConfig` with a fixed config, recording every + * `tx` it was called with so tests can assert the read hit the resolved TAR with the + * expected calldata (the read is this op's only authorization gate, so it earns its own + * assertion rather than passing implicitly whenever the config happens to come back right). + */ +function stubProvider(config: { + administrator?: string + pendingAdministrator?: string + tokenPool?: string +}) { + const calls: { to?: string; data?: string }[] = [] + return { + calls, + call: (tx: { to?: string; data?: string }) => { + calls.push(tx) + return Promise.resolve(encodeTokenConfig(config)) + }, + } +} + +/** Minimal EVMChain stub — the build path resolves the TAR, then reads `getTokenConfig` off `provider`. */ +function stubChain(overrides: Partial = {}): EVMChain { + return { + provider: stubProvider({ pendingAdministrator: SENDER }), + logger: { debug() {}, info() {}, warn() {}, error() {} }, + getTokenAdminRegistryFor: (_address: string) => Promise.resolve(TAR), + nextNonce: async () => 0, + rollbackNonce: () => {}, + ...overrides, + } as unknown as EVMChain +} + +/** Fake ethers Signer for a plain (non-deployment) tx. */ +function fakeSigner(opts: { waitError?: Error } = {}) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(SENDER), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => + opts.waitError + ? Promise.reject(opts.waitError) + : Promise.resolve({ status: 1, contractAddress: null }), + }), + } +} + +describe('AcceptAdmin (cct/evm token-admin-registry operation)', () => { + describe('generate (golden vectors)', () => { + it('encodes acceptAdminRole(token) to the discovered TAR when sender is pending', async () => { + const provider = stubProvider({ pendingAdministrator: SENDER }) + const unsigned = await new AcceptAdmin().generate( + stubChain({ provider: provider as never }), + { + tokenAddress: TOKEN, + address: ADDRESS, + sender: SENDER, + }, + ) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal(tx.to, TAR) + assert.equal(tx.from, SENDER) + assert.ok(tx.data!.startsWith(SELECTOR), 'data carries the acceptAdminRole selector') + assert.equal(tx.data, SELECTOR + word(TOKEN)) + + // The read is this op's only authorization gate — assert it actually hit the resolved + // TAR with `getTokenConfig(tokenAddress)`, not just that some read returned a config + // that happened to satisfy the pending-admin check. + assert.equal(provider.calls.length, 1) + assert.equal(provider.calls[0]!.to, TAR) + assert.equal( + provider.calls[0]!.data, + interfaces.TokenAdminRegistry.encodeFunctionData('getTokenConfig', [TOKEN]), + ) + }) + + it('discovers the TAR from the given address', async () => { + let seen: string | undefined + const unsigned = await new AcceptAdmin().generate( + stubChain({ + getTokenAdminRegistryFor: (address: string) => { + seen = address + return Promise.resolve(TAR) + }, + }), + { tokenAddress: TOKEN, address: ADDRESS, sender: SENDER }, + ) + assert.equal(seen, ADDRESS) + assert.equal(unsigned.transactions[0]!.to, TAR) + }) + + it('matches a checksum-insensitive sender against the pending administrator', async () => { + // pendingAdministrator decodes checksummed off-chain; a lowercase sender must still match. + const unsigned = await new AcceptAdmin().generate( + stubChain({ provider: stubProvider({ pendingAdministrator: SENDER }) as never }), + { tokenAddress: TOKEN, address: ADDRESS, sender: SENDER.toLowerCase() }, + ) + assert.equal(unsigned.transactions[0]!.data, SELECTOR + word(TOKEN)) + }) + }) + + describe('validation', () => { + it('rejects an invalid tokenAddress before any RPC', async () => { + let called = false + await assert.rejects( + () => + new AcceptAdmin().generate( + stubChain({ + getTokenAdminRegistryFor: () => { + called = true + return Promise.resolve(TAR) + }, + }), + { tokenAddress: 'not-an-address', address: ADDRESS, sender: SENDER }, + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'acceptAdmin' && + err.context.param === 'tokenAddress', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + + it('rejects an invalid address', async () => { + await assert.rejects( + () => + new AcceptAdmin().generate(stubChain(), { + tokenAddress: TOKEN, + address: 'not-an-address', + sender: SENDER, + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'address', + ) + }) + + it('rejects a missing sender', async () => { + await assert.rejects( + () => new AcceptAdmin().generate(stubChain(), { tokenAddress: TOKEN, address: ADDRESS }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects an invalid sender', async () => { + await assert.rejects( + () => + new AcceptAdmin().generate(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + sender: 'not-an-address', + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects the zero address written in ICAP form as sender', async () => { + // isAddress() accepts ICAP, and this never equals ZeroAddress literally + await assert.rejects( + () => + new AcceptAdmin().generate(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + sender: getIcapAddress(ZeroAddress), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects when no administrator is pending', async () => { + await assert.rejects( + () => + new AcceptAdmin().generate( + stubChain({ + provider: stubProvider({ administrator: OTHER }) as never, // pendingAdministrator omitted -> zero + }), + { tokenAddress: TOKEN, address: ADDRESS, sender: SENDER }, + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'acceptAdmin' && + err.context.param === 'sender' && + /nothing to accept/.test(err.message), + ) + }) + + it('rejects when sender is not the pending administrator', async () => { + await assert.rejects( + () => + new AcceptAdmin().generate( + stubChain({ provider: stubProvider({ pendingAdministrator: OTHER }) as never }), + { tokenAddress: TOKEN, address: ADDRESS, sender: SENDER }, + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + /must be the pending token administrator/.test(err.message), + ) + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await new AcceptAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + sender: SENDER, + wallet: fakeSigner(), + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('throws CCIPExecTxRevertedError when the tx reverts on-chain', async () => { + await assert.rejects( + () => + new AcceptAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + sender: SENDER, + wallet: fakeSigner({ waitError: makeError('execution reverted', 'CALL_EXCEPTION') }), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && err.context.operation === 'acceptAdmin', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => + new AcceptAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + sender: SENDER, + wallet: {}, + }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + + it('defaults sender to the executing wallet address when omitted', async () => { + // fakeSigner().getAddress() resolves to SENDER, which stubChain()'s provider also + // reports as pendingAdministrator — so an omitted `sender` must still pass the + // pending-administrator pre-check by binding to the wallet. + const result = await new AcceptAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + wallet: fakeSigner(), + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('binds a lowercase sender to a checksummed wallet address', async () => { + // The wallet-binding comparison normalises both sides with getAddress(). Without that, a + // lowercase `sender` — the shape that comes out of indexers, subgraphs and `toLowerCase()` + // pipelines — would read as a different address from the checksummed one the signer reports, + // and the legitimate pending administrator would be rejected as "not the executing wallet". + // fakeSigner() reports the checksummed SENDER. + const result = await new AcceptAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + sender: SENDER.toLowerCase(), + wallet: fakeSigner(), + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a malformed sender with CCTParamsInvalidError, not a raw ethers error', async () => { + // The execute override compares addresses before the base generate()'s validate() runs, + // so it must validate first — otherwise getAddress() leaks an ethers TypeError. + await assert.rejects( + () => + new AcceptAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + sender: 'not-an-address', + wallet: fakeSigner(), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'acceptAdmin' && + err.context.param === 'sender', + ) + }) + + it('rejects a sender that does not match the executing wallet', async () => { + // Regression guard: a caller-supplied `sender` must bind to the address that actually + // signs. `fakeSigner()` resolves to SENDER, which stubChain()'s provider also reports as + // pendingAdministrator — so absent this check, `sender: OTHER` would sail through the + // pending-administrator pre-check (SENDER === SENDER) yet broadcast from a signer whose + // on-chain `msg.sender` doesn't match, reverting with `OnlyPendingAdministrator` instead + // of failing fast here. + await assert.rejects( + () => + new AcceptAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + sender: OTHER, + wallet: fakeSigner(), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'acceptAdmin' && + err.context.param === 'sender' && + /executing wallet address/.test(err.message), + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin.ts new file mode 100644 index 00000000..e83ba6cc --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin.ts @@ -0,0 +1,114 @@ +/** + * acceptAdmin — accepts a pending TokenAdminRegistry administrator role for a token. + * Second half of the two-step admin handshake: `registerAdmin` (fresh registration) or + * `transferAdmin` (existing-admin hand-off) first proposes an address as + * `pendingAdministrator`; that address then calls `acceptAdmin` to become `administrator`, + * after which `setPool` is callable. Version-independent (v1.5–v2.0 share one encoding). + * + * @packageDocumentation + */ + +import { ZeroAddress, getAddress } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { type EVMExecuteParams, EVMOperation, callTx } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' +import { getTokenAdminRegistryInterface, readTokenAdminRegistryConfig } from '../contracts.ts' + +/** + * Parameters for `acceptAdmin`. + * @remarks `sender` is typed optional to satisfy `EVMOperation`'s shared shape, but is required + * for {@link AcceptAdmin.generate}: the pre-tx check below has nothing to compare + * `pendingAdministrator` against without it, so an omitted `sender` is rejected in + * {@link AcceptAdmin.validate}. {@link AcceptAdmin.execute} relaxes this — it defaults `sender` + * to the signing wallet's own address, since that is the only address that can ever satisfy + * the pending-administrator check for a signed submission (see {@link AcceptAdmin.execute}). + */ +export type AcceptAdminParams = { + /** Token whose pending registry admin role is being accepted. */ + tokenAddress: string + /** + * Contract to resolve the TokenAdminRegistry from. Pass the registry itself for a + * direct lookup; a Router, OnRamp, OffRamp, or TokenPool also work but add hops and + * need a configured lane. + */ + address: string + /** + * Pending administrator accepting the role. Required for {@link AcceptAdmin.generate} + * (unsigned/offline flows); optional for {@link AcceptAdmin.execute}, which defaults it to + * the wallet's address — see the remarks above. + */ + sender?: string +} + +/** Accepts a pending TokenAdminRegistry administrator role for a token. */ +export class AcceptAdmin extends EVMOperation { + readonly name = 'acceptAdmin' + + /** + * Validates all addresses before any RPC. `sender` is required here (unlike the base + * `EVMOperation` shape) — see the {@link AcceptAdminParams} remarks. + */ + protected validate(p: AcceptAdminParams): void { + validateAddress(this.name, 'tokenAddress', p.tokenAddress) + validateAddress(this.name, 'address', p.address) + validateAddress(this.name, 'sender', p.sender) + } + + /** + * Confirms `sender` is the pending administrator, then builds `acceptAdminRole` calldata + * against the TokenAdminRegistry resolved from `address`. + */ + protected async buildUnsigned(chain: EVMChain, p: AcceptAdminParams): Promise { + // Asserts and narrows in one step — `sender` is optional on EVMOperation's shared shape, and + // `validate()`'s guarantee doesn't survive the hop into this method. + validateAddress(this.name, 'sender', p.sender) + const sender = getAddress(p.sender) + const to = await chain.getTokenAdminRegistryFor(p.address) + const { administrator, pendingAdministrator } = await readTokenAdminRegistryConfig( + chain, + to, + p.tokenAddress, + ) + + if (pendingAdministrator === ZeroAddress) { + throw new CCTParamsInvalidError( + this.name, + 'sender', + `no administrator is pending for this token (current administrator: ${administrator}) — nothing to accept`, + ) + } + if (pendingAdministrator !== sender) { + throw new CCTParamsInvalidError( + this.name, + 'sender', + `must be the pending token administrator (${pendingAdministrator})`, + ) + } + + // TAR.acceptAdminRole encoding is version-stable across v1.5–v2.0; no version dispatch needed. + const data = getTokenAdminRegistryInterface().encodeFunctionData('acceptAdminRole', [ + p.tokenAddress, + ]) + return callTx(to, data) + } + + /** + * Signs and submits as the pending administrator, defaulting `sender` to the signing wallet — + * the only address that can satisfy {@link buildUnsigned}'s pending-administrator check for a + * broadcast tx. See {@link EVMOperation.senderBoundToWallet} for why a divergent `sender` is + * rejected rather than signed. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if `sender` is given and is not the wallet's address + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.senderBoundToWallet(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-supported-tokens.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-supported-tokens.test.ts new file mode 100644 index 00000000..0f6150cb --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-supported-tokens.test.ts @@ -0,0 +1,126 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { GetSupportedTokens } from './get-supported-tokens.ts' +import { interfaces } from '../../../../evm/const.ts' +import { EVMChain } from '../../../../evm/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const OFF_RAMP = '0x' + '11'.repeat(20) +const TAR = '0x' + '22'.repeat(20) +const TOKENS = ['0x' + '33'.repeat(20), '0x' + '44'.repeat(20)] + +describe('GetSupportedTokens (cct/evm)', () => { + describe('query', () => { + it('resolves the TAR and lists its configured tokens', async () => { + let resolvedAddress: string | undefined + let seenOpts: { page?: number } | undefined + const chain = { + getTokenAdminRegistryFor: async (address: string) => { + resolvedAddress = address + return TAR + }, + getSupportedTokens: async (registry: string, opts?: { page?: number }) => { + assert.equal(registry, TAR) + seenOpts = opts + return TOKENS + }, + } as unknown as EVMChain + + const result = await new GetSupportedTokens().query(chain, { address: OFF_RAMP }) + assert.deepEqual(result, TOKENS) + assert.equal(resolvedAddress, OFF_RAMP) + assert.deepEqual(seenOpts, { page: undefined }) + }) + + it('forwards `page` to chain.getSupportedTokens, which owns the pagination loop', async () => { + let seenPage: number | undefined + const chain = { + getTokenAdminRegistryFor: async () => TAR, + getSupportedTokens: async (_registry: string, opts?: { page?: number }) => { + seenPage = opts?.page + return TOKENS + }, + } as unknown as EVMChain + + await new GetSupportedTokens().query(chain, { address: OFF_RAMP, page: 50 }) + assert.equal(seenPage, 50) + }) + + it('paginates `getAllConfiguredTokens` through the real EVMChain.getSupportedTokens loop', async () => { + // The two tests above stub `chain.getSupportedTokens` wholesale, so they only pin that this + // op forwards `page` — they cannot catch a startIndex/maxCount swap or a dropped final page + // in the loop itself. This test runs the *real* `EVMChain.prototype.getSupportedTokens` + // (bound to a stub with just `provider.call`) against three tokens with `page: 2`, so a full + // first page (0,2) must be followed by a short second page (2,2) that ends the scan. + const allTokens = [...TOKENS, '0x' + '55'.repeat(20)] + const seenCalls: Array<{ startIndex: bigint; maxCount: bigint }> = [] + const chain = { + getTokenAdminRegistryFor: async () => TAR, + provider: { + call: async ({ data }: { data: string }) => { + const [startIndex, maxCount] = interfaces.TokenAdminRegistry.decodeFunctionData( + 'getAllConfiguredTokens', + data, + ) as unknown as [bigint, bigint] + seenCalls.push({ startIndex, maxCount }) + const page = allTokens.slice(Number(startIndex), Number(startIndex + maxCount)) + return interfaces.TokenAdminRegistry.encodeFunctionResult('getAllConfiguredTokens', [ + page, + ]) + }, + }, + } as unknown as EVMChain + chain.getSupportedTokens = EVMChain.prototype.getSupportedTokens.bind(chain) + + const result = await new GetSupportedTokens().query(chain, { address: OFF_RAMP, page: 2 }) + + assert.deepEqual(result, allTokens, 'pages are concatenated in order') + assert.deepEqual( + seenCalls, + [ + { startIndex: 0n, maxCount: 2n }, + { startIndex: 2n, maxCount: 2n }, + ], + 'startIndex advances by the previous page length and maxCount stays pinned to `page`', + ) + }) + }) + + describe('validation', () => { + it('rejects an invalid address before any RPC', async () => { + let called = false + const chain = { + getTokenAdminRegistryFor: async () => { + called = true + return TAR + }, + } as unknown as EVMChain + + await assert.rejects( + () => new GetSupportedTokens().query(chain, { address: 'not-an-address' }), + (error: unknown) => + error instanceof CCTParamsInvalidError && + error.context.operation === 'getSupportedTokens' && + error.context.param === 'address', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + + it('rejects a non-positive `page`', async () => { + await assert.rejects( + () => new GetSupportedTokens().query({} as EVMChain, { address: OFF_RAMP, page: 0 }), + (error: unknown) => + error instanceof CCTParamsInvalidError && error.context.param === 'page', + ) + }) + + it('rejects a non-integer `page`', async () => { + await assert.rejects( + () => new GetSupportedTokens().query({} as EVMChain, { address: OFF_RAMP, page: 1.5 }), + (error: unknown) => + error instanceof CCTParamsInvalidError && error.context.param === 'page', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-supported-tokens.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-supported-tokens.ts new file mode 100644 index 00000000..00d2418b --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-supported-tokens.ts @@ -0,0 +1,68 @@ +/** + * getSupportedTokens — lists the ERC-20 tokens configured in a TokenAdminRegistry. Version-independent + * (`getAllConfiguredTokens` is byte-identical from v1.5.0 through latest). + * + * @packageDocumentation + */ + +import type { EVMChain } from '../../../../evm/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMQuery } from '../../query.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for {@link GetSupportedTokens}. */ +export type GetSupportedTokensParams = { + /** + * Contract to resolve the TokenAdminRegistry from. Pass the registry itself for a + * direct lookup; a Router, OnRamp, OffRamp, or TokenPool also work but add hops and + * need a configured lane. + */ + address: string + /** + * Batch size `chain.getSupportedTokens` requests per `getAllConfiguredTokens` call while it + * paginates. Defaults to 1000. Optional — a very large registry may need a smaller batch to + * stay under an RPC's response-size limit. + */ + page?: number +} + +/** Result of {@link GetSupportedTokens}: array of token addresses. */ +export type GetSupportedTokensResult = string[] + +/** + * Lists every token configured in the TokenAdminRegistry resolved from `address`, paginating + * through `getAllConfiguredTokens` until exhausted. + */ +export class GetSupportedTokens extends EVMQuery { + readonly name = 'getSupportedTokens' + + /** + * Validates the resolution address and, when given, `page`; nothing to convert for {@link read}. + * @throws {@link CCTParamsInvalidError} if `address` is not a valid address, or `page` is given + * and is not a positive integer + */ + protected prepare(params: GetSupportedTokensParams): GetSupportedTokensParams { + validateAddress(this.name, 'address', params.address) + if (params.page !== undefined && !(Number.isInteger(params.page) && params.page > 0)) + throw new CCTParamsInvalidError( + this.name, + 'page', + `must be a positive integer, got ${String(params.page)}`, + ) + return params + } + + /** + * Resolves the TAR and lists its configured tokens. + * @remarks Delegates pagination to {@link EVMChain.getSupportedTokens}, which already loops + * `getAllConfiguredTokens(startIndex, maxCount)` until a short page ends the scan — this op does + * not reimplement that loop. + */ + protected async read( + chain: EVMChain, + { address, page }: GetSupportedTokensParams, + ): Promise { + const registry = await chain.getTokenAdminRegistryFor(address) + return chain.getSupportedTokens(registry, { page }) + } +} diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-token-admin-registry.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-token-admin-registry.test.ts new file mode 100644 index 00000000..0258a3ca --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-token-admin-registry.test.ts @@ -0,0 +1,181 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, getAddress, makeError } from 'ethers' + +import { GetTokenAdminRegistry } from './get-token-admin-registry.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const ROUTER = '0x' + '22'.repeat(20) +const TAR = '0x' + '33'.repeat(20) +// Mixed-case hex, unlike TOKEN/ROUTER/TAR above: their EIP-55 checksums re-case letters, so +// asserting `getAddress(FIXTURE)` below only proves normalization happens if the raw fixture +// isn't already in checksummed form. A digit-only fixture would pass even if the op forgot to +// checksum (or lowercased) its output. +const ADMINISTRATOR = '0xabcdef1234567890abcdef1234567890abcdef12' +const PENDING_ADMINISTRATOR = '0x1234567890abcdef1234567890abcdef12345678' +const POOL = '0xfedcba9876543210fedcba9876543210fedcba98' + +const IFACE = new Interface([ + 'function getTokenConfig(address token) view returns (tuple(address administrator, address pendingAdministrator, address tokenPool))', +]) + +/** + * EVMChain stub: `getTokenAdminRegistryFor` reports `registry`, and the provider answers + * `eth_call` with `getTokenConfig` encoded as `(administrator, pendingAdministrator, tokenPool)` — + * but only for a call to `registry` decoding to `TOKEN`; any other call, target, or argument + * reverts (or fails the assertion), so a read that mixes up its target or argument is caught + * rather than silently returning the fixture data. + */ +function stubChain({ + registry = TAR, + administrator = ADMINISTRATOR, + pendingAdministrator = PENDING_ADMINISTRATOR, + tokenPool = POOL, +}: { + registry?: string + administrator?: string + pendingAdministrator?: string + tokenPool?: string +} = {}): EVMChain { + const encoded = IFACE.encodeFunctionResult('getTokenConfig', [ + [administrator, pendingAdministrator, tokenPool], + ]) + const selector = IFACE.getFunction('getTokenConfig')!.selector + + return { + provider: { + call: async ({ to, data }: { to?: string; data: string }) => { + if (data.slice(0, 10) !== selector) + throw makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: null, data }, + invocation: null, + revert: null, + }) + // Selector-only matching can't tell a correct read from one with the call target or the + // decoded token argument swapped (e.g. reading a different contract's, or a different + // token's, config) — both would still hit this branch and get `encoded` back. Assert the + // resolved registry and the decoded argument so either mix-up fails loudly instead of + // silently returning the fixture data. + assert.equal(to, getAddress(registry), 'calls the resolved TAR, not `address`') + const [token] = IFACE.decodeFunctionData('getTokenConfig', data) + assert.equal(token, getAddress(TOKEN), 'reads the config for `tokenAddress`') + return encoded + }, + }, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + getTokenAdminRegistryFor: () => Promise.resolve(registry), + } as unknown as EVMChain +} + +describe('GetTokenAdminRegistry (cct/evm token-admin-registry query)', () => { + it('reads administrator, pendingAdministrator, and tokenPool', async () => { + const config = await new GetTokenAdminRegistry().query(stubChain(), { + address: ROUTER, + tokenAddress: TOKEN, + }) + + assert.deepEqual(config, { + administrator: getAddress(ADMINISTRATOR), + pendingAdministrator: getAddress(PENDING_ADMINISTRATOR), + tokenPool: getAddress(POOL), + }) + }) + + it('resolves the TAR from `address` before reading', async () => { + let seen: string | undefined + const chain = stubChain() + chain.getTokenAdminRegistryFor = (address: string) => { + seen = address + return Promise.resolve(TAR) + } + + await new GetTokenAdminRegistry().query(chain, { address: ROUTER, tokenAddress: TOKEN }) + + assert.equal(seen, ROUTER) + }) + + it( + 'reports a zero administrator rather than throwing — the pending-registration state ' + + 'EVMChain.getRegistryTokenConfig cannot observe', + async () => { + const config = await new GetTokenAdminRegistry().query( + stubChain({ administrator: ZeroAddress }), + { address: ROUTER, tokenAddress: TOKEN }, + ) + + assert.equal(config.administrator, ZeroAddress) + assert.equal(config.pendingAdministrator, getAddress(PENDING_ADMINISTRATOR)) + }, + ) + + it('omits pendingAdministrator when zero', async () => { + const config = await new GetTokenAdminRegistry().query( + stubChain({ pendingAdministrator: ZeroAddress }), + { address: ROUTER, tokenAddress: TOKEN }, + ) + + assert.ok(!('pendingAdministrator' in config)) + }) + + it('omits tokenPool when zero', async () => { + const config = await new GetTokenAdminRegistry().query(stubChain({ tokenPool: ZeroAddress }), { + address: ROUTER, + tokenAddress: TOKEN, + }) + + assert.ok(!('tokenPool' in config)) + }) + + it('omits both optional fields for an unregistered token', async () => { + const config = await new GetTokenAdminRegistry().query( + stubChain({ + administrator: ZeroAddress, + pendingAdministrator: ZeroAddress, + tokenPool: ZeroAddress, + }), + { address: ROUTER, tokenAddress: TOKEN }, + ) + + assert.deepEqual(config, { administrator: ZeroAddress }) + }) + + describe('validation', () => { + it('rejects an invalid `address` before any RPC', async () => { + let called = false + const chain = stubChain() + chain.getTokenAdminRegistryFor = () => { + called = true + return Promise.resolve(TAR) + } + + await assert.rejects( + () => new GetTokenAdminRegistry().query(chain, { address: 'nope', tokenAddress: TOKEN }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'getTokenAdminRegistry' && + err.context.param === 'address', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + + it('rejects an invalid `tokenAddress` before any RPC', async () => { + await assert.rejects( + () => + new GetTokenAdminRegistry().query(stubChain(), { + address: ROUTER, + tokenAddress: 'nope', + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'getTokenAdminRegistry' && + err.context.param === 'tokenAddress', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-token-admin-registry.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-token-admin-registry.ts new file mode 100644 index 00000000..06344480 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/get-token-admin-registry.ts @@ -0,0 +1,84 @@ +/** + * getTokenAdminRegistry — reads a token's TokenAdminRegistry entry: its administrator, any + * pending administrator, and its registered pool. Version-independent (v1.5–v2.0 share one + * `getTokenConfig` encoding). + * + * @packageDocumentation + */ + +import { ZeroAddress } from 'ethers' + +import type { RegistryTokenConfig } from '../../../../chain.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { EVMQuery } from '../../query.ts' +import { validateAddress } from '../../validate.ts' +import { readTokenAdminRegistryConfig } from '../contracts.ts' + +/** Parameters for {@link GetTokenAdminRegistry}. */ +export type GetTokenAdminRegistryParams = { + /** + * Contract to resolve the TokenAdminRegistry from. Pass the registry itself for a + * direct lookup; a Router, OnRamp, OffRamp, or TokenPool also work but add hops and + * need a configured lane. + */ + address: string + /** Token to read the registry entry for. */ + tokenAddress: string +} + +/** + * Result of {@link GetTokenAdminRegistry}: the TAR entry for one token. + * @remarks `administrator` may be {@link ZeroAddress} for a token pending acceptance; + * test with `=== ZeroAddress`, not truthiness. + */ +export type GetTokenAdminRegistryResult = RegistryTokenConfig + +/** + * Reads a token's TokenAdminRegistry entry directly through `getTokenConfig`, reporting a zero + * `administrator` rather than throwing. + * @remarks Deliberately diverges from `EVMChain.getRegistryTokenConfig`, which throws + * `CCIPTokenNotConfiguredError` whenever `administrator === ZeroAddress` — exactly the + * post-`registerAdmin`, pre-`acceptAdmin` state, which made a pending registration + * unobservable through the public read API. This op reads `getTokenConfig` through + * {@link readTokenAdminRegistryConfig} instead of delegating to that helper, so + * `{ administrator: ZeroAddress, pendingAdministrator }` is reported faithfully. + * `pendingAdministrator` and `tokenPool` are still omitted when zero (nothing pending, no pool + * registered) — only `administrator` survives as the zero address, since that is the one state + * this op exists to surface. + */ +export class GetTokenAdminRegistry extends EVMQuery< + GetTokenAdminRegistryParams, + GetTokenAdminRegistryResult +> { + readonly name = 'getTokenAdminRegistry' + + /** + * Validates both addresses; nothing to convert for {@link read}. + * @throws {@link CCTParamsInvalidError} if `address` or `tokenAddress` is not a valid address + */ + protected prepare(params: GetTokenAdminRegistryParams): GetTokenAdminRegistryParams { + validateAddress(this.name, 'address', params.address) + validateAddress(this.name, 'tokenAddress', params.tokenAddress) + return params + } + + /** Resolves the TAR from `address`, then reads and normalizes `getTokenConfig(tokenAddress)`. */ + protected async read( + chain: EVMChain, + { address, tokenAddress }: GetTokenAdminRegistryParams, + ): Promise { + const registry = await chain.getTokenAdminRegistryFor(address) + const config = await readTokenAdminRegistryConfig(chain, registry, tokenAddress) + + return { + // unlike EVMChain.getRegistryTokenConfig, a zero administrator is reported, not thrown — + // that's the whole point of this op (see the class @remarks). The two optional fields are + // dropped when zero, so callers can test presence rather than compare against ZeroAddress. + administrator: config.administrator, + ...(config.pendingAdministrator !== ZeroAddress && { + pendingAdministrator: config.pendingAdministrator, + }), + ...(config.tokenPool !== ZeroAddress && { tokenPool: config.tokenPool }), + } + } +} diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/register-admin.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/register-admin.test.ts new file mode 100644 index 00000000..7c124173 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/register-admin.test.ts @@ -0,0 +1,656 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, getAddress, id, makeError } from 'ethers' + +import { RegisterAdmin } from './register-admin.ts' +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { + CCTContractTypeInvalidError, + CCTContractVersionUnsupportedError, + CCTParamsInvalidError, +} from '../../../errors.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const REGISTRY_MODULE = '0x' + '22'.repeat(20) +const ROUTER = '0x' + '33'.repeat(20) +const TAR = '0x' + '44'.repeat(20) +// Deliberately letter-bearing, so its checksummed and lowercase spellings differ. The +// already-registered assertions below feed the stub a lowercase administrator and assert the +// error carries the checksummed form — which is what pins `readTokenAdminRegistryConfig`'s +// checksumming. With an all-digit fixture the two spellings coincide and that guarantee is +// silently untested. +const ADMIN = getAddress('0x' + 'ad'.repeat(20)) +const OTHER = '0x' + '66'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) +const ROLE = '0x' + '00'.repeat(32) // OZ's DEFAULT_ADMIN_ROLE constant (bytes32(0)) + +// Module-call golden vectors, written by hand against a locally-declared Interface rather than +// the vendored REGISTRY_MODULE_OWNER_CUSTOM_ABI, so this stays an independent check: swapping the +// encoded argument (e.g. registryModule instead of token) or the wrong moduleFn would show up here +// even though it'd also validate cleanly against the (correct) vendored ABI. +const GOLDEN_MODULE_INTERFACE = new Interface([ + 'function registerAdminViaOwner(address token)', + 'function registerAdminViaGetCCIPAdmin(address token)', + 'function registerAccessControlDefaultAdmin(address token)', +]) +const OWNER_DATA = GOLDEN_MODULE_INTERFACE.encodeFunctionData('registerAdminViaOwner', [TOKEN]) +const CCIP_ADMIN_DATA = GOLDEN_MODULE_INTERFACE.encodeFunctionData('registerAdminViaGetCCIPAdmin', [ + TOKEN, +]) +const ACCESS_CONTROL_DATA = GOLDEN_MODULE_INTERFACE.encodeFunctionData( + 'registerAccessControlDefaultAdmin', + [TOKEN], +) + +// TAR-side selectors probed by pre-tx validation. +const IS_REGISTRY_MODULE_SELECTOR = + interfaces.TokenAdminRegistry.getFunction('isRegistryModule')!.selector +const GET_TOKEN_CONFIG_SELECTOR = + interfaces.TokenAdminRegistry.getFunction('getTokenConfig')!.selector + +// Token-side selectors, independently derived via `id(...)` rather than read back from the +// throwaway Interfaces the op itself builds — so a wrong-getter mutation in the op can't also +// silently rewrite the expectation. +const OWNER_GETTER_SELECTOR = id('owner()').slice(0, 10) +const CCIP_ADMIN_GETTER_SELECTOR = id('getCCIPAdmin()').slice(0, 10) +const DEFAULT_ADMIN_ROLE_SELECTOR = id('DEFAULT_ADMIN_ROLE()').slice(0, 10) +const HAS_ROLE_SELECTOR = id('hasRole(bytes32,address)').slice(0, 10) + +/** Throwaway single-fragment interface for a token getter, mirroring the op's own probe. */ +const getterInterface = (name: string) => + new Interface([`function ${name}() view returns (address)`]) +/** Mirrors the op's own throwaway AccessControl interface, used to decode recorded calls. */ +const accessControlInterface = new Interface([ + 'function DEFAULT_ADMIN_ROLE() view returns (bytes32)', + 'function hasRole(bytes32, address) view returns (bool)', +]) + +type TokenConfig = { administrator: string; pendingAdministrator: string; tokenPool: string } + +const UNREGISTERED: TokenConfig = { + administrator: ZeroAddress, + pendingAdministrator: ZeroAddress, + tokenPool: ZeroAddress, +} + +/** A recorded `provider.call`: its target and raw calldata, for asserting *where* a probe went. */ +type RecordedCall = { to: string | undefined; data: string } + +/** + * Minimal EVMChain stub with a selector-aware `provider.call`, mirroring `finality-preflight.test.ts`. + * Pass `calls` to record every call `{ to, data }` — needed because the per-method getter mapping + * is otherwise untestable: `encodeFunctionResult` for a `() view returns (address)` fragment + * produces identical bytes regardless of the function name, so only the *request* (selector + + * target), not the stubbed response, can prove which getter was actually probed. Any selector this + * stub doesn't recognise throws (rather than falling back to a generic response), so a probe aimed + * at the wrong function or the wrong address fails loudly instead of returning a plausible value. + */ +function stubChain( + opts: { + isModule?: boolean + tokenConfig?: TokenConfig + getter?: string + getterAddress?: string + hasRole?: boolean + moduleTypeAndVersion?: [string, string] + calls?: RecordedCall[] + overrides?: Partial + } = {}, +): EVMChain { + const isModule = opts.isModule ?? true + const tokenConfig = opts.tokenConfig ?? UNREGISTERED + const getter = opts.getter ?? 'owner' + const getterAddress = opts.getterAddress ?? ADMIN + const hasRole = opts.hasRole ?? true + const [moduleType, moduleVersion] = opts.moduleTypeAndVersion ?? [ + 'RegistryModuleOwnerCustom', + '1.6.0', + ] + + const provider = { + call: async (tx: { to?: string; data?: string }) => { + const data = tx.data ?? '0x' + const sel = data.slice(0, 10) + opts.calls?.push({ to: tx.to, data }) + // Recording alone leaves the TAR-side probes unpinned unless a test bothers to inspect + // `calls`. Asserting here instead pins them for EVERY test: without this, swapping an + // argument (e.g. `getTokenConfig(registryModule)`, which silently disables the + // already-registered guard) or aiming a probe at the wrong contract keeps the suite green. + const at = (label: string, expected: string) => + assert.equal(getAddress(tx.to ?? ZeroAddress), getAddress(expected), `${label} target`) + if (sel === IS_REGISTRY_MODULE_SELECTOR) { + at('isRegistryModule', TAR) + const [mod] = interfaces.TokenAdminRegistry.decodeFunctionData( + 'isRegistryModule', + data, + ) as unknown as [string] + assert.equal( + getAddress(mod), + getAddress(REGISTRY_MODULE), + 'isRegistryModule asks about `registryModule`', + ) + return interfaces.TokenAdminRegistry.encodeFunctionResult('isRegistryModule', [isModule]) + } + if (sel === GET_TOKEN_CONFIG_SELECTOR) { + at('getTokenConfig', TAR) + const [tok] = interfaces.TokenAdminRegistry.decodeFunctionData( + 'getTokenConfig', + data, + ) as unknown as [string] + assert.equal(getAddress(tok), getAddress(TOKEN), 'getTokenConfig asks about `tokenAddress`') + return interfaces.TokenAdminRegistry.encodeFunctionResult('getTokenConfig', [ + [tokenConfig.administrator, tokenConfig.pendingAdministrator, tokenConfig.tokenPool], + ]) + } + // Every token-side probe must read the token itself, never the module or the registry. + if (sel === DEFAULT_ADMIN_ROLE_SELECTOR) { + at('DEFAULT_ADMIN_ROLE', TOKEN) + return accessControlInterface.encodeFunctionResult('DEFAULT_ADMIN_ROLE', [ROLE]) + } + if (sel === HAS_ROLE_SELECTOR) { + at('hasRole', TOKEN) + return accessControlInterface.encodeFunctionResult('hasRole', [hasRole]) + } + if (sel === getterInterface(getter).getFunction(getter)!.selector) { + at(`${getter}()`, TOKEN) + return getterInterface(getter).encodeFunctionResult(getter, [getterAddress]) + } + throw new Error(`stubChain: unrecognised selector ${sel} at ${tx.to ?? '(no to)'}`) + }, + } + + return { + provider, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + getTokenAdminRegistryFor: (_address: string) => Promise.resolve(TAR), + typeAndVersion: (_address: string) => + Promise.resolve([moduleType, moduleVersion, `${moduleType} ${moduleVersion}`]), + nextNonce: async () => 0, + rollbackNonce: () => {}, + ...opts.overrides, + } as unknown as EVMChain +} + +/** Fake ethers Signer for a plain (non-deployment) tx. */ +function fakeSigner(opts: { address?: string; waitError?: Error } = {}) { + const address = opts.address ?? ADMIN + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => + opts.waitError + ? Promise.reject(opts.waitError) + : Promise.resolve({ status: 1, contractAddress: null }), + }), + } +} + +describe('RegisterAdmin (cct/evm token-admin-registry operation)', () => { + describe('generate (golden vectors)', () => { + it('defaults to owner and encodes registerAdminViaOwner(token) to the module', async () => { + const unsigned = await new RegisterAdmin().generate(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + sender: ADMIN, + }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal(tx.to, REGISTRY_MODULE) + assert.equal(tx.from, ADMIN) + // Full calldata, not just the selector — catches a wrong-argument encode (e.g. the + // registryModule address instead of the token) that `startsWith(selector)` would miss. + assert.equal(tx.data, OWNER_DATA) + }) + + it('encodes registerAdminViaGetCCIPAdmin(token) for ccip-admin', async () => { + const unsigned = await new RegisterAdmin().generate(stubChain({ getter: 'getCCIPAdmin' }), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + registrationMethod: 'ccip-admin', + sender: ADMIN, + }) + assert.equal(unsigned.transactions[0]!.data, CCIP_ADMIN_DATA) + }) + + it('encodes registerAccessControlDefaultAdmin(token) for access-control-default-admin', async () => { + const unsigned = await new RegisterAdmin().generate(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + registrationMethod: 'access-control-default-admin', + sender: ADMIN, + }) + assert.equal(unsigned.transactions[0]!.data, ACCESS_CONTROL_DATA) + }) + + it('discovers the TAR from the router address', async () => { + let seen: string | undefined + const unsigned = await new RegisterAdmin().generate( + stubChain({ + overrides: { + getTokenAdminRegistryFor: (address: string) => { + seen = address + return Promise.resolve(TAR) + }, + }, + }), + { tokenAddress: TOKEN, registryModule: REGISTRY_MODULE, address: ROUTER }, + ) + assert.equal(seen, ROUTER) + assert.ok(unsigned) + }) + + it('omits `from` when no sender is given (and skips the getter probe)', async () => { + const unsigned = await new RegisterAdmin().generate(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + }) + assert.equal(unsigned.transactions[0]!.from, undefined) + }) + }) + + describe('per-method token-side probe', () => { + // These assert the *request* (selector + target), not just a stubbed return value, since + // `stubChain`'s per-method responses are otherwise indistinguishable (see its doc comment). + // This is the coverage that would have caught the `access-control-default-admin` blocker: a + // probe against the wrong selector (`defaultAdmin()`) shows up directly instead of being + // absorbed by a catch-all stub response. + + it('probes owner() at the token (not the module) for the default method', async () => { + const calls: RecordedCall[] = [] + await new RegisterAdmin().generate(stubChain({ calls }), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + sender: ADMIN, + }) + const probe = calls.find((c) => c.data.slice(0, 10) === OWNER_GETTER_SELECTOR) + assert.ok(probe, 'owner() was probed') + assert.equal(probe.to, TOKEN) + }) + + it('probes getCCIPAdmin() at the token for ccip-admin', async () => { + const calls: RecordedCall[] = [] + await new RegisterAdmin().generate(stubChain({ getter: 'getCCIPAdmin', calls }), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + registrationMethod: 'ccip-admin', + sender: ADMIN, + }) + const probe = calls.find((c) => c.data.slice(0, 10) === CCIP_ADMIN_GETTER_SELECTOR) + assert.ok(probe, 'getCCIPAdmin() was probed') + assert.equal(probe.to, TOKEN) + }) + + it('probes DEFAULT_ADMIN_ROLE()/hasRole(role, sender) at the token for access-control-default-admin', async () => { + const calls: RecordedCall[] = [] + await new RegisterAdmin().generate(stubChain({ calls }), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + registrationMethod: 'access-control-default-admin', + sender: ADMIN, + }) + + const roleCall = calls.find((c) => c.data.slice(0, 10) === DEFAULT_ADMIN_ROLE_SELECTOR) + assert.ok(roleCall, 'DEFAULT_ADMIN_ROLE() was probed') + assert.equal(roleCall.to, TOKEN) + + const hasRoleCall = calls.find((c) => c.data.slice(0, 10) === HAS_ROLE_SELECTOR) + assert.ok(hasRoleCall, 'hasRole(role, sender) was probed') + assert.equal(hasRoleCall.to, TOKEN) + const [role, account] = accessControlInterface.decodeFunctionData('hasRole', hasRoleCall.data) + assert.equal(role, ROLE) + assert.equal(account, ADMIN) + }) + }) + + describe('validation', () => { + it('rejects an invalid tokenAddress before any RPC', async () => { + let called = false + await assert.rejects( + () => + new RegisterAdmin().generate( + stubChain({ + overrides: { + getTokenAdminRegistryFor: () => ((called = true), Promise.resolve(TAR)), + }, + }), + { tokenAddress: 'nope', registryModule: REGISTRY_MODULE, address: ROUTER }, + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'registerAdmin' && + err.context.param === 'tokenAddress', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + + it('rejects an invalid registryModule', async () => { + await assert.rejects( + () => + new RegisterAdmin().generate(stubChain(), { + tokenAddress: TOKEN, + registryModule: 'nope', + address: ROUTER, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'registryModule', + ) + }) + + it('rejects an invalid address', async () => { + await assert.rejects( + () => + new RegisterAdmin().generate(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: 'nope', + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'address', + ) + }) + + it('rejects an unrecognised registrationMethod', async () => { + await assert.rejects( + () => + new RegisterAdmin().generate(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + registrationMethod: 'nope' as never, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'registrationMethod', + ) + }) + + it('rejects a registryModule the TAR does not recognise', async () => { + await assert.rejects( + () => + new RegisterAdmin().generate(stubChain({ isModule: false }), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'registerAdmin' && + err.context.param === 'registryModule', + ) + }) + + it('rejects a declared version that disagrees with the module on-chain', async () => { + // `registryModuleVersion` defaults to 1.6.0, so this declares 1.6.0 against a 1.5.0 module. + // Both versions encode the shared functions identically, so nothing downstream would notice — + // the resolved version is what makes the compile-time narrowing true. + await assert.rejects( + () => + new RegisterAdmin().generate( + stubChain({ moduleTypeAndVersion: ['RegistryModuleOwnerCustom', '1.5.0'] }), + { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + registrationMethod: 'access-control-default-admin', + }, + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'registryModuleVersion' && + typeof err.context.reason === 'string' && + err.context.reason.includes('v1.5.0'), + ) + }) + + it('accepts a v1.5.0 module when that version is declared', async () => { + // The union removes `access-control-default-admin` from `registrationMethod` here, so only + // the two getter-derived paths are even expressible. + const unsigned = await new RegisterAdmin().generate( + stubChain({ moduleTypeAndVersion: ['RegistryModuleOwnerCustom', '1.5.0'] }), + { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + registryModuleVersion: '1.5.0', + sender: ADMIN, + }, + ) + assert.equal(unsigned.transactions[0]!.data, OWNER_DATA) + }) + + it('rejects an address that is not a RegistryModuleOwnerCustom', async () => { + await assert.rejects( + () => + new RegisterAdmin().generate( + stubChain({ moduleTypeAndVersion: ['TokenPool', '1.6.0'] }), + { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + }, + ), + (err: unknown) => err instanceof CCTContractTypeInvalidError, + ) + }) + + it('rejects a module reporting an unknown version', async () => { + await assert.rejects( + () => + new RegisterAdmin().generate( + stubChain({ moduleTypeAndVersion: ['RegistryModuleOwnerCustom', '9.9.9'] }), + { tokenAddress: TOKEN, registryModule: REGISTRY_MODULE, address: ROUTER }, + ), + (err: unknown) => err instanceof CCTContractVersionUnsupportedError, + ) + }) + + it('rejects when sender does not match the token getter for the method', async () => { + await assert.rejects( + () => + new RegisterAdmin().generate(stubChain({ getterAddress: OTHER }), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + sender: ADMIN, + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects when sender lacks DEFAULT_ADMIN_ROLE for access-control-default-admin', async () => { + await assert.rejects( + () => + new RegisterAdmin().generate(stubChain({ hasRole: false }), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + registrationMethod: 'access-control-default-admin', + sender: ADMIN, + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a token already registered (administrator set)', async () => { + await assert.rejects( + () => + new RegisterAdmin().generate( + stubChain({ + tokenConfig: { + administrator: ADMIN.toLowerCase(), + pendingAdministrator: ZeroAddress, + tokenPool: ZeroAddress, + }, + }), + { tokenAddress: TOKEN, registryModule: REGISTRY_MODULE, address: ROUTER }, + ), + // The two already-registered cases carry different remediation (hand the role over vs + // wait for the pending admin to accept), so each pins its own message — asserting only + // `param` would let the branches be swapped without any test noticing. + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'tokenAddress' && + typeof err.context.reason === 'string' && + err.context.reason.includes('already has registry administrator') && + err.context.reason.includes(ADMIN), + ) + }) + + it('rejects a token with a pending registration (administrator still zero)', async () => { + await assert.rejects( + () => + new RegisterAdmin().generate( + stubChain({ + tokenConfig: { + administrator: ZeroAddress, + pendingAdministrator: ADMIN.toLowerCase(), + tokenPool: ZeroAddress, + }, + }), + { tokenAddress: TOKEN, registryModule: REGISTRY_MODULE, address: ROUTER }, + ), + // Stricter than the contract on purpose: proposeAdministrator would silently overwrite a + // pending proposal (it only reverts once `administrator` is non-zero), so this guard is + // the SDK's, and its message must name the address waiting to accept. + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'tokenAddress' && + typeof err.context.reason === 'string' && + err.context.reason.includes('already pending') && + err.context.reason.includes(ADMIN), + ) + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await new RegisterAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + wallet: fakeSigner(), + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('throws CCIPExecTxRevertedError when the tx reverts on-chain', async () => { + await assert.rejects( + () => + new RegisterAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + wallet: fakeSigner({ waitError: makeError('execution reverted', 'CALL_EXCEPTION') }), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && err.context.operation === 'registerAdmin', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => + new RegisterAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + wallet: {}, + }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + + it('defaults sender to the wallet address, rejecting a wallet that is not the token owner', async () => { + // The default `execute({ ...params, wallet })` shape — no explicit `sender` — is exactly + // the path that must not skip the authority check (see `RegisterAdmin.execute`'s doc + // comment). `stubChain()`'s owner() resolves to ADMIN; this wallet is OTHER. + await assert.rejects( + () => + new RegisterAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + wallet: fakeSigner({ address: OTHER }), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'registerAdmin' && + err.context.param === 'sender', + ) + }) + + it('rejects a sender that differs from the signing wallet', async () => { + // Uniform with transferAdmin/acceptAdmin: the module gates on the wallet's msg.sender, so + // honouring a divergent `sender` would reduce the authority pre-check to advice — the call + // would pass every local guard and still revert on-chain. Offline/multisig signers use + // generateUnsignedRegisterAdmin, where `sender` is trusted as given. + await assert.rejects( + () => + new RegisterAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + sender: ADMIN, + wallet: fakeSigner({ address: OTHER }), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'registerAdmin' && + err.context.param === 'sender' && + // pins the builder name senderBoundToWallet derives from `this.name`, so the shared + // helper can't start telling registerAdmin callers to use some other method + typeof err.context.reason === 'string' && + err.context.reason.includes('generateUnsignedRegisterAdmin'), + ) + }) + + it('rejects a malformed sender with CCTParamsInvalidError, not a raw ethers error', async () => { + // senderBoundToWallet validates before getAddress(), which would otherwise throw a raw + // ethers TypeError. That guard runs ahead of generate()'s own validate(), so nothing else + // covers it — without this test, deleting it leaves the suite green and silently breaks the + // documented error taxonomy for every op sharing the helper. + await assert.rejects( + () => + new RegisterAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + sender: 'not-an-address', + wallet: fakeSigner({ address: ADMIN }), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'registerAdmin' && + err.context.param === 'sender', + ) + }) + + it('accepts a sender matching the signing wallet', async () => { + // The redundant-but-explicit call shape: passing `sender` equal to the wallet is allowed, so + // callers who thread `sender` through both builders and executors need no special-casing. + const result = await new RegisterAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + registryModule: REGISTRY_MODULE, + address: ROUTER, + sender: ADMIN, + wallet: fakeSigner({ address: ADMIN }), + }) + assert.deepEqual(result, { hash: HASH }) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/register-admin.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/register-admin.ts new file mode 100644 index 00000000..e454efbc --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/register-admin.ts @@ -0,0 +1,259 @@ +/** + * registerAdmin — proposes a token's administrator in the TokenAdminRegistry (TAR) by calling a + * RegistryModuleOwnerCustom, one of three self-service paths CCIP ships so a token owner never + * needs the TAR owner's help to onboard. Two-step by design, like `transferAdmin`: the token + * lands in `pendingAdministrator` until the proposed administrator calls `acceptAdmin`. + * + * @packageDocumentation + */ + +import { Contract, Interface, ZeroAddress, getAddress } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { type EVMExecuteParams, EVMOperation, callTx } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' +import { + RegistryModuleOwnerCustomVersion, + getRegistryModuleOwnerCustomInterface, + isRegistryModule, + readTokenAdminRegistryConfig, + resolveRegistryModuleOwnerCustom, +} from '../contracts.ts' + +/** + * Self-service authorization paths a RegistryModuleOwnerCustom accepts, each proving control of + * the token through a different on-chain getter rather than a signature the module has to verify + * itself. Defaults to `owner`, the common case for a plain `Ownable` token. + */ +const REGISTRATION_METHODS = { + OWNER: 'owner', + CCIP_ADMIN: 'ccip-admin', + ACCESS_CONTROL_DEFAULT_ADMIN: 'access-control-default-admin', +} as const + +/** Authorization path used to register a token's administrator via a RegistryModuleOwnerCustom. */ +export type RegisterAdminMethod = (typeof REGISTRATION_METHODS)[keyof typeof REGISTRATION_METHODS] + +/** + * Per-method wiring: the RegistryModuleOwnerCustom function this op calls. `owner`/`ccip-admin` + * also carry the token getter whose return value the module registers as administrator and + * checks against the caller (`_registerAdmin`'s `admin != msg.sender` revert) — used here to + * pre-flight that same equality. `access-control-default-admin` has no such getter: unlike the + * other two, `registerAccessControlDefaultAdmin` never derives an address from the token at all — + * it checks `AccessControl(token).hasRole(DEFAULT_ADMIN_ROLE(), msg.sender)` and then registers + * `msg.sender` itself, so it's pre-flighted as a role check in {@link RegisterAdmin.buildUnsigned} + * rather than through a `tokenGetter` here. + */ +const REGISTRATION: Record< + RegisterAdminMethod, + { readonly moduleFn: string; readonly tokenGetter?: string } +> = { + [REGISTRATION_METHODS.OWNER]: { moduleFn: 'registerAdminViaOwner', tokenGetter: 'owner' }, + [REGISTRATION_METHODS.CCIP_ADMIN]: { + moduleFn: 'registerAdminViaGetCCIPAdmin', + tokenGetter: 'getCCIPAdmin', + }, + [REGISTRATION_METHODS.ACCESS_CONTROL_DEFAULT_ADMIN]: { + moduleFn: 'registerAccessControlDefaultAdmin', + }, +} + +/** Registration paths a v1.5.0 module offers — both derive the administrator from a token getter. */ +export type RegisterAdminMethodV1_5_0 = Exclude + +/** Fields every registration path needs, whatever the module version. */ +type RegisterAdminBaseParams = { + /** Token to register. Stays unregistered until `acceptAdmin` is called by the proposed admin. */ + tokenAddress: string + /** + * `RegistryModuleOwnerCustom` to call. The TAR exposes `isRegistryModule` but no enumeration, + * so — unlike `address` below — this can't be discovered on-chain and must be supplied. + */ + registryModule: string + /** + * Contract to resolve the TokenAdminRegistry from. Pass the registry itself for a direct + * lookup; a Router, OnRamp, OffRamp, or TokenPool also work but add hops and need a + * configured lane. + */ + address: string + /** + * Address the registration is authorized against. Optional here, unlike `transferAdmin` and + * `acceptAdmin` which reject an omitted `sender`: leaving it out SKIPS the token-authority probe + * in {@link RegisterAdmin.buildUnsigned}, so the tx builds without that check and can then only + * fail on-chain. {@link RegisterAdmin.execute} defaults it to the signing wallet. + */ + sender?: string +} + +/** + * Registration through a v1.5.0 `RegistryModuleOwnerCustom` — that version has no + * `registerAccessControlDefaultAdmin`, so `registrationMethod` narrows to the two getter-derived + * paths and the AccessControl one will not typecheck. + */ +export type RegisterAdminParamsV1_5_0 = RegisterAdminBaseParams & { + registryModuleVersion: typeof RegistryModuleOwnerCustomVersion.V1_5_0 + /** Selects which token getter proves control; defaults to `owner`. */ + registrationMethod?: RegisterAdminMethodV1_5_0 +} + +/** + * Registration through a v1.6.0 `RegistryModuleOwnerCustom` — the default, and the only version + * offering `access-control-default-admin`. + */ +export type RegisterAdminParamsV1_6_0 = RegisterAdminBaseParams & { + registryModuleVersion?: typeof RegistryModuleOwnerCustomVersion.V1_6_0 + /** Selects how control is proved; defaults to `owner`. */ + registrationMethod?: RegisterAdminMethod +} + +/** + * Parameters for {@link RegisterAdmin}, discriminated on `registryModuleVersion`: `1.5.0` drops + * `access-control-default-admin` (a compile-time guarantee); omit it for the `1.6.0` default. + * {@link RegisterAdmin.buildUnsigned} verifies the declaration against the module's on-chain + * version. The administrator itself is never a parameter — see {@link REGISTRATION}. + */ +export type RegisterAdminParams = RegisterAdminParamsV1_5_0 | RegisterAdminParamsV1_6_0 + +/** + * Proposes a token's administrator in the TokenAdminRegistry via a RegistryModuleOwnerCustom. + * For `owner`/`ccip-admin` the module — not this op — derives the administrator from the token + * itself; for `access-control-default-admin` it registers the caller once a role check passes. + */ +export class RegisterAdmin extends EVMOperation { + readonly name = 'registerAdmin' + + /** Validates addresses and, if given, `registrationMethod`; no RPC. */ + protected validate(p: RegisterAdminParams): void { + validateAddress(this.name, 'tokenAddress', p.tokenAddress) + validateAddress(this.name, 'registryModule', p.registryModule) + validateAddress(this.name, 'address', p.address) + if ( + p.registrationMethod !== undefined && + !Object.values(REGISTRATION_METHODS).includes(p.registrationMethod) + ) { + throw new CCTParamsInvalidError( + this.name, + 'registrationMethod', + `must be one of ${Object.values(REGISTRATION_METHODS).join(', ')}`, + ) + } + } + + /** + * Resolves the TAR, then runs the on-chain checks that would otherwise surface as an opaque + * revert, before encoding the module call. + */ + protected async buildUnsigned(chain: EVMChain, p: RegisterAdminParams): Promise { + const method = p.registrationMethod ?? REGISTRATION_METHODS.OWNER + const { moduleFn } = REGISTRATION[method] + + const registry = await chain.getTokenAdminRegistryFor(p.address) + + // The TAR reverts `OnlyRegistryModuleOrOwner` from deep inside the module call; check here. + if (!(await isRegistryModule(chain, registry, p.registryModule))) { + throw new CCTParamsInvalidError( + this.name, + 'registryModule', + `${p.registryModule} is not a registered module on the TokenAdminRegistry at ${registry}`, + ) + } + + // Both versions encode the shared functions identically, so a wrong `registryModuleVersion` + // would go unnoticed until the module rejected the call. Resolve and compare instead. + const onChainVersion = await resolveRegistryModuleOwnerCustom(chain, p.registryModule) + const declaredVersion = p.registryModuleVersion ?? RegistryModuleOwnerCustomVersion.V1_6_0 + if (onChainVersion !== declaredVersion) { + throw new CCTParamsInvalidError( + this.name, + 'registryModuleVersion', + `${p.registryModule} is a v${onChainVersion} RegistryModuleOwnerCustom, but v${declaredVersion} was declared`, + ) + } + + // Pre-flight the module's own authorization check (see REGISTRATION), so a mismatch fails + // here rather than as a `CanOnlySelfRegister`/`RequiredRoleNotFound` revert. Needs `sender`. + if (p.sender !== undefined) { + if (method === REGISTRATION_METHODS.ACCESS_CONTROL_DEFAULT_ADMIN) { + // Not `defaultAdmin()`: that lives on `AccessControlDefaultAdminRules`, not the plain + // `AccessControl` the module casts to. Mirror the module: read the role, then `hasRole`. + const accessControlInterface = new Interface([ + 'function DEFAULT_ADMIN_ROLE() view returns (bytes32)', + 'function hasRole(bytes32, address) view returns (bool)', + ]) + const token = new Contract(p.tokenAddress, accessControlInterface, chain.provider) + const role = (await token.getFunction('DEFAULT_ADMIN_ROLE')()) as string + const hasRole = (await token.getFunction('hasRole')(role, p.sender)) as boolean + if (!hasRole) { + throw new CCTParamsInvalidError( + this.name, + 'sender', + `must hold the token's DEFAULT_ADMIN_ROLE (AccessControl.hasRole) for registrationMethod "access-control-default-admin"`, + ) + } + } else { + const tokenGetter = REGISTRATION[method].tokenGetter! + const tokenGetterInterface = new Interface([ + `function ${tokenGetter}() view returns (address)`, + ]) + const admin = (await new Contract( + p.tokenAddress, + tokenGetterInterface, + chain.provider, + ).getFunction(tokenGetter)()) as string + if (getAddress(admin) !== getAddress(p.sender)) { + throw new CCTParamsInvalidError( + this.name, + 'sender', + `must equal token.${tokenGetter}() (${admin}) for registrationMethod "${method}"`, + ) + } + } + } + + // `proposeAdministrator` reverts `AlreadyRegistered` only once `administrator` is non-zero; + // a pending proposal is silently overwritten. Rejecting that too is deliberately stricter. + const { administrator, pendingAdministrator } = await readTokenAdminRegistryConfig( + chain, + registry, + p.tokenAddress, + ) + if (administrator !== ZeroAddress) { + throw new CCTParamsInvalidError( + this.name, + 'tokenAddress', + `token already has registry administrator ${administrator} — use transferAdmin to hand the role over, or setPool if you are already the admin`, + ) + } + if (pendingAdministrator !== ZeroAddress) { + throw new CCTParamsInvalidError( + this.name, + 'tokenAddress', + `a registration proposing ${pendingAdministrator} is already pending — that address must call acceptAdmin (re-registering would silently replace the proposal)`, + ) + } + + const data = getRegistryModuleOwnerCustomInterface(onChainVersion).encodeFunctionData( + moduleFn, + [p.tokenAddress], + ) + return callTx(p.registryModule, data) + } + + /** + * Signs and submits as the token's authority, defaulting `sender` to the signing wallet — the + * only address the module's `msg.sender` check can pass. See + * {@link EVMOperation.senderBoundToWallet} for why a divergent `sender` is rejected. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if `sender` is given and is not the wallet's address + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.senderBoundToWallet(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.test.ts new file mode 100644 index 00000000..3cf7eb9e --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.test.ts @@ -0,0 +1,156 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import { type SetPoolParams, SetPool } from './set-pool.ts' +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const POOL = '0x' + '22'.repeat(20) +const ADDRESS = '0x' + '33'.repeat(20) +const TAR = '0x' + '44'.repeat(20) +const SENDER = '0x' + '55'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +const DATA = new Interface([ + 'function setPool(address localToken, address pool)', +]).encodeFunctionData('setPool', [TOKEN, POOL]) + +function stubChain(onAddress?: (address: string) => void): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + getTokenAdminRegistryFor: (address: string) => { + onAddress?.(address) + return Promise.resolve(TAR) + }, + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(waitError?: Error) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(SENDER), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => (waitError ? Promise.reject(waitError) : Promise.resolve({ status: 1 })), + }), + } +} + +const op = new SetPool() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { + tokenAddress: TOKEN, + poolAddress: POOL, + address: ADDRESS, + sender: SENDER, + ...overrides, + }) +} + +describe('SetPool (cct/evm)', () => { + describe('generate', () => { + it('encodes setPool(token, pool) to the discovered TAR', async () => { + const unsigned = await generate(stubChain()) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, TAR) + assert.equal(tx.from, SENDER) + assert.equal(tx.data, DATA) + }) + + it('discovers the TAR from address', async () => { + let seen: string | undefined + await generate(stubChain((address) => (seen = address))) + assert.equal(seen, ADDRESS) + }) + + it('omits from when sender is not supplied', async () => { + const unsigned = await generate(stubChain(), { sender: undefined }) + assert.equal(unsigned.transactions[0]!.from, undefined) + }) + + it('allows the zero pool address to delist a token', async () => { + const unsigned = await generate(stubChain(), { poolAddress: ZeroAddress }) + assert.equal( + unsigned.transactions[0]!.data, + new Interface(['function setPool(address localToken, address pool)']).encodeFunctionData( + 'setPool', + [TOKEN, ZeroAddress], + ), + ) + }) + }) + + describe('validation', () => { + for (const param of ['tokenAddress', 'poolAddress', 'address', 'sender'] as const) { + it(`rejects an invalid ${param} before TAR discovery`, async () => { + let called = false + await assert.rejects( + () => + generate( + stubChain(() => (called = true)), + { [param]: 'not-an-address' }, + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setPool' && + err.context.param === param, + ) + assert.equal(called, false) + }) + } + }) + + describe('execute', () => { + it('signs and submits, resolving to the tx hash', async () => { + assert.deepEqual( + await op.execute(stubChain(), { + tokenAddress: TOKEN, + poolAddress: POOL, + address: ADDRESS, + wallet: fakeSigner(), + }), + { hash: HASH }, + ) + }) + + it('maps an on-chain revert to CCIPExecTxRevertedError', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + poolAddress: POOL, + address: ADDRESS, + wallet: fakeSigner(makeError('execution reverted', 'CALL_EXCEPTION')), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && err.context.operation === 'setPool', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + poolAddress: POOL, + address: ADDRESS, + wallet: {}, + }), + CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.ts new file mode 100644 index 00000000..65760703 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.ts @@ -0,0 +1,49 @@ +/** + * setPool — registers a pool for a token in the TokenAdminRegistry. + * Version-independent (v1.5–v2.0 share one encoding). + * + * @packageDocumentation + */ + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { EVMOperation, callTx } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' +import { getTokenAdminRegistryInterface } from '../contracts.ts' + +/** Parameters for `setPool`. Zero `poolAddress` delists the token. */ +export type SetPoolParams = { + tokenAddress: string + /** The zero address as `poolAddress` delists the token from the registry. */ + poolAddress: string + /** + * Contract to resolve the TokenAdminRegistry from. Pass the registry itself for a + * direct lookup; a Router, OnRamp, OffRamp, or TokenPool also work but add hops and + * need a configured lane. + */ + address: string + sender?: string +} + +/** Registers a pool for a token in the TokenAdminRegistry resolved from `address`. */ +export class SetPool extends EVMOperation { + readonly name = 'setPool' + + /** Validates all addresses before any RPC. */ + protected validate(p: SetPoolParams): void { + validateAddress(this.name, 'tokenAddress', p.tokenAddress) + validateAddress(this.name, 'poolAddress', p.poolAddress) + validateAddress(this.name, 'address', p.address) + } + + /** Builds `setPool` calldata against the TokenAdminRegistry resolved from `address`. */ + protected async buildUnsigned(chain: EVMChain, p: SetPoolParams): Promise { + const to = await chain.getTokenAdminRegistryFor(p.address) + // TAR.setPool encoding is version-stable across v1.5–v2.0; no version dispatch needed. + const data = getTokenAdminRegistryInterface().encodeFunctionData('setPool', [ + p.tokenAddress, + p.poolAddress, + ]) + return callTx(to, data) + } +} diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin.test.ts new file mode 100644 index 00000000..a5f18af5 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin.test.ts @@ -0,0 +1,301 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ZeroAddress, getAddress } from 'ethers' + +import { type TransferAdminParams, TransferAdmin } from './transfer-admin.ts' +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const ADDRESS = '0x' + '22'.repeat(20) +const TAR = '0x' + '33'.repeat(20) +const CURRENT_ADMIN = '0x' + '44'.repeat(20) +const NEW_ADMIN = '0x' + '55'.repeat(20) +const OTHER = '0x' + '66'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +const TRANSFER_ADMIN_ROLE_SELECTOR = + interfaces.TokenAdminRegistry.getFunction('transferAdminRole')!.selector +const EXPECTED_DATA = interfaces.TokenAdminRegistry.encodeFunctionData('transferAdminRole', [ + TOKEN, + NEW_ADMIN, +]) + +/** Encodes a `getTokenConfig` result the way the on-chain TAR would. */ +function encodeTokenConfig( + administrator: string, + pendingAdministrator = ZeroAddress, + tokenPool = ZeroAddress, +) { + return interfaces.TokenAdminRegistry.encodeFunctionResult('getTokenConfig', [ + [administrator, pendingAdministrator, tokenPool], + ]) +} + +/** + * Minimal EVMChain stub — a fake provider answers `getTokenConfig` reads via `call`. + * @remarks The provider asserts *what* it was asked rather than answering blindly, so every test in + * this file pins the read: a mutation that points the authorization pre-check at the wrong contract + * (e.g. the registry module instead of the resolved TAR) or at the wrong token would otherwise keep + * the whole suite green. `decodeFunctionData` also rejects a wrong-function mutation outright. + */ +function stubChain( + administrator = CURRENT_ADMIN, + opts: { + pendingAdministrator?: string + onAddress?: (address: string) => void + /** Token the pre-check is expected to read; defaults to the token under test. */ + expectToken?: string + } = {}, +): EVMChain { + return { + provider: { + call: async (tx: { to?: string; data?: string }) => { + assert.equal( + getAddress(tx.to ?? ZeroAddress), + getAddress(TAR), + 'pre-check must read the TAR resolved from `address`', + ) + const [readToken] = interfaces.TokenAdminRegistry.decodeFunctionData( + 'getTokenConfig', + tx.data ?? '0x', + ) as unknown as [string] + assert.equal( + getAddress(readToken), + getAddress(opts.expectToken ?? TOKEN), + 'pre-check must read the config of the token being transferred', + ) + return encodeTokenConfig(administrator, opts.pendingAdministrator) + }, + }, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + getTokenAdminRegistryFor: (address: string) => { + opts.onAddress?.(address) + return Promise.resolve(TAR) + }, + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +/** Fake ethers Signer whose broadcast resolves to a confirmed receipt. */ +function fakeSigner(address = CURRENT_ADMIN) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ hash: HASH, wait: () => Promise.resolve({ status: 1 }) }), + } +} + +const op = new TransferAdmin() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + address: ADDRESS, + sender: CURRENT_ADMIN, + ...overrides, + }) +} + +describe('TransferAdmin (cct/evm)', () => { + describe('generate', () => { + it('encodes transferAdminRole(token, newAdmin) to the discovered TAR', async () => { + const unsigned = await generate(stubChain()) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + + const tx = unsigned.transactions[0]! + assert.equal(tx.to, TAR) + assert.equal(tx.from, CURRENT_ADMIN) + assert.ok( + tx.data!.startsWith(TRANSFER_ADMIN_ROLE_SELECTOR), + 'data starts with transferAdminRole selector', + ) + assert.equal(tx.data, EXPECTED_DATA) + }) + + it('discovers the TAR from the address param', async () => { + let seen: string | undefined + await generate(stubChain(CURRENT_ADMIN, { onAddress: (address) => (seen = address) })) + assert.equal(seen, ADDRESS) + }) + }) + + describe('validation', () => { + it('rejects an invalid tokenAddress before any RPC, tagged with the operation', async () => { + let called = false + const chain = stubChain(CURRENT_ADMIN, { onAddress: () => (called = true) }) + await assert.rejects( + () => generate(chain, { tokenAddress: 'not-an-address' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'transferAdmin' && + err.context.param === 'tokenAddress', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + + it('rejects an invalid newAdmin', async () => { + await assert.rejects( + () => generate(stubChain(), { newAdmin: 'not-an-address' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'newAdmin', + ) + }) + + it('rejects an invalid address', async () => { + await assert.rejects( + () => generate(stubChain(), { address: 'not-an-address' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'address', + ) + }) + + it('rejects a missing sender', async () => { + await assert.rejects( + () => generate(stubChain(), { sender: undefined }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a sender that is not the current administrator', async () => { + await assert.rejects( + () => generate(stubChain(CURRENT_ADMIN), { sender: OTHER }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + typeof err.context.reason === 'string' && + err.context.reason.includes('must be the current token administrator'), + ) + }) + + it('rejects a token that is not registered', async () => { + await assert.rejects( + () => generate(stubChain(ZeroAddress), { sender: OTHER }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + typeof err.context.reason === 'string' && + err.context.reason.includes('is not registered'), + ) + }) + + it('distinguishes a registration still pending acceptance from not-registered', async () => { + await assert.rejects( + () => + generate(stubChain(ZeroAddress, { pendingAdministrator: NEW_ADMIN }), { + sender: OTHER, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + typeof err.context.reason === 'string' && + err.context.reason.includes('still pending acceptance') && + err.context.reason.includes(NEW_ADMIN), + ) + }) + + it('rejects a zero-address sender on an unregistered token', async () => { + // Regression: the guard compared `administrator !== sender` before judging registration + // state, so a zero `sender` — which validateAddress permits — compared equal to an + // unregistered token's zero `administrator` and slipped past all three checks, emitting a + // transferAdminRole tx for a token with no admin to transfer. Registration state must be + // decided first, independently of who `sender` is. + await assert.rejects( + () => generate(stubChain(ZeroAddress), { sender: ZeroAddress }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + typeof err.context.reason === 'string' && + err.context.reason.includes('is not registered'), + ) + }) + + it('rejects a zero-address sender on a token still pending acceptance', async () => { + // Same bypass, but the pending branch: still must not build, and must say why. + await assert.rejects( + () => + generate(stubChain(ZeroAddress, { pendingAdministrator: NEW_ADMIN }), { + sender: ZeroAddress, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + typeof err.context.reason === 'string' && + err.context.reason.includes('still pending acceptance'), + ) + }) + }) + + describe('execute', () => { + it('signs and submits, resolving to the confirmed tx hash', async () => { + const result = await op.execute(stubChain(), { + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + address: ADDRESS, + sender: CURRENT_ADMIN, + wallet: fakeSigner(CURRENT_ADMIN), + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('defaults sender to the wallet address when omitted', async () => { + // Uniform with registerAdmin/acceptAdmin: `sender` is required for generate() (buildUnsigned + // must know who to authorize against before encoding), but execute() can always derive it + // from the wallet — the only address that can satisfy the current-administrator check for a + // signed submission. Omitting it must therefore succeed, not fail validation. + const result = await op.execute(stubChain(), { + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + address: ADDRESS, + wallet: fakeSigner(CURRENT_ADMIN), + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + address: ADDRESS, + sender: CURRENT_ADMIN, + wallet: {}, + }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + + it('rejects sender not matching the executing wallet, without reading the registry', async () => { + let readRegistry = false + const chain = stubChain(CURRENT_ADMIN, { onAddress: () => (readRegistry = true) }) + await assert.rejects( + () => + op.execute(chain, { + tokenAddress: TOKEN, + newAdmin: NEW_ADMIN, + address: ADDRESS, + // sender is a valid administrator, but not the address that `wallet` signs with — + // the registry read alone can't catch this (submit() clears tx.from before + // populate), so execute must compare sender against the wallet directly. + sender: CURRENT_ADMIN, + wallet: fakeSigner(OTHER), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + typeof err.context.reason === 'string' && + err.context.reason.includes('must be the executing wallet'), + ) + assert.equal(readRegistry, false, 'rejected before reading the registry / broadcasting') + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin.ts new file mode 100644 index 00000000..a09f3b1a --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin.ts @@ -0,0 +1,139 @@ +/** + * transferAdmin — proposes a new TokenAdminRegistry administrator for a token + * (two-step; the proposed admin must separately call `acceptAdmin`). + * Version-independent (v1.5–v2.0 share one encoding). + * + * @remarks This is the registry's ADMIN role — the account allowed to call `setPool` + * ({@link SetPool}) and manage the token's CCT configuration in the `TokenAdminRegistry`. + * It is entirely distinct from a `TokenPool`'s Ownable2Step *owner* ({@link TransferOwnership}), + * which controls the pool contract itself (rate limits, remote-chain config, etc.). A token's + * registry admin and its pool's owner are commonly the same EOA/multisig, but the two roles + * live on different contracts and are transferred independently — do not confuse `transferAdmin` + * (this op) with `transferOwnership`. + * + * @packageDocumentation + */ + +import { ZeroAddress, getAddress } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { type EVMExecuteParams, EVMOperation, callTx } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' +import { getTokenAdminRegistryInterface, readTokenAdminRegistryConfig } from '../contracts.ts' + +/** + * Parameters for {@link TransferAdmin}. + * @remarks `sender` is typed optional to satisfy `EVMOperation`'s shared shape, but is required + * for {@link TransferAdmin.generate}: the pre-tx check below has nothing to compare + * `administrator` against without it, so an omitted `sender` is rejected in + * {@link TransferAdmin.validate}. {@link TransferAdmin.execute} relaxes this — it defaults + * `sender` to the signing wallet's own address, the only address that can satisfy the + * current-administrator check for a signed submission (see {@link TransferAdmin.execute}). + */ +export type TransferAdminParams = { + /** Token whose registry admin role is being handed over. */ + tokenAddress: string + /** The administrator proposed to accept the token's registry admin role. Pass {@link ZeroAddress} + * to cancel any pending transfer — the pending proposal is discarded without accepting the role. + */ + newAdmin: string + /** + * Contract to resolve the TokenAdminRegistry from. Pass the registry itself for a + * direct lookup; a Router, OnRamp, OffRamp, or TokenPool also work but add hops and + * need a configured lane. + */ + address: string + /** + * Current registry administrator. Required for {@link TransferAdmin.generate} + * (unsigned/offline flows) — `buildUnsigned` must read the registry and confirm the caller is + * the current administrator *before* encoding a tx, so it needs to know who that caller is up + * front. Optional for {@link TransferAdmin.execute}, which defaults it to the wallet's address + * — see the remarks above. + */ + sender?: string +} + +/** + * Proposes a new TokenAdminRegistry administrator for a token via `transferAdminRole`. + * Two-step by design: `newAdmin` must separately call `acceptAdmin` to complete the handoff — + * this op alone does not change who can act as administrator. + */ +export class TransferAdmin extends EVMOperation { + readonly name = 'transferAdmin' + + /** Validates all addresses before any RPC, including the presence of `sender` (see above). */ + protected validate(p: TransferAdminParams): void { + validateAddress(this.name, 'tokenAddress', p.tokenAddress) + validateAddress(this.name, 'newAdmin', p.newAdmin) + validateAddress(this.name, 'address', p.address) + validateAddress(this.name, 'sender', p.sender) + } + + /** + * Reads the registry directly, confirms `sender` is the current administrator, then builds + * `transferAdminRole` calldata against the TAR resolved from `address`. + */ + protected async buildUnsigned(chain: EVMChain, p: TransferAdminParams): Promise { + const to = await chain.getTokenAdminRegistryFor(p.address) + const { administrator, pendingAdministrator } = await readTokenAdminRegistryConfig( + chain, + to, + p.tokenAddress, + ) + + // Asserts and narrows in one step — `sender` is optional on EVMOperation's shared shape, and + // `validate()`'s guarantee doesn't survive the hop into this method. + validateAddress(this.name, 'sender', p.sender) + const sender = getAddress(p.sender) + const pending = pendingAdministrator === ZeroAddress ? undefined : pendingAdministrator + + // Registration state is checked BEFORE comparing against `sender`, and deliberately so: an + // unregistered token has a zero `administrator`, so an equality-first check would let + // `sender: ZeroAddress` (which validateAddress permits) compare equal to it and build a + // `transferAdminRole` tx for a token that has no admin to transfer. + if (administrator === ZeroAddress) { + throw new CCTParamsInvalidError( + this.name, + 'sender', + pending + ? `registration for this token is still pending acceptance by ${pending}; the pending administrator must accept the admin role first — this operation only transfers an accepted role` + : `token ${p.tokenAddress} is not registered in the TokenAdminRegistry at ${to}; call registerAdmin first`, + ) + } + if (administrator !== sender) { + throw new CCTParamsInvalidError( + this.name, + 'sender', + `must be the current token administrator (${administrator})`, + ) + } + + // TAR.transferAdminRole encoding is version-stable across v1.5–v2.0; no version dispatch needed. + const data = getTokenAdminRegistryInterface().encodeFunctionData('transferAdminRole', [ + p.tokenAddress, + p.newAdmin, + ]) + chain.logger.debug(`${this.name}: registry = ${to}, token = ${p.tokenAddress}`) + return callTx(to, data) + } + + /** + * Signs and submits as the current administrator, defaulting `sender` to the signing wallet — + * the only address that can satisfy {@link buildUnsigned}'s current-administrator check for a + * broadcast tx. See {@link EVMOperation.senderBoundToWallet} for why a divergent `sender` is + * rejected rather than signed. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if `sender` is given and is not the wallet's address, or + * if any other param is invalid (see {@link buildUnsigned}) + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.senderBoundToWallet(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts b/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts new file mode 100644 index 00000000..79ef9d48 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts @@ -0,0 +1,233 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface } from 'ethers' + +import { + TOKEN_POOL_FAMILIES, + TOKEN_POOL_INTERFACES, + TOKEN_POOL_TYPES, + TokenPoolVersion, + getTokenPoolFamily, + getTokenPoolInterface, + isLockReleaseTokenPoolType, + isTokenPoolType, + isTokenPoolVersion, + parseTokenPoolVersion, + resolveEncoder, +} from './contracts.ts' +import { + CCTContractTypeInvalidError, + CCTContractVersionUnsupportedError, + CCTOperationUnsupportedError, +} from '../../errors.ts' + +const ADDR = '0x' + '11'.repeat(20) + +describe('pool types', () => { + it('lists known EVM pool types (burn family + lock release)', () => { + assert.deepEqual( + [...TOKEN_POOL_TYPES].sort(), + [ + 'BurnFromMintTokenPool', + 'BurnMintTokenPool', + 'BurnMintWithLockReleaseFlagTokenPool', + 'BurnToAddressTokenPool', + 'BurnWithFromMintTokenPool', + 'LockReleaseTokenPool', + 'SiloedLockReleaseTokenPool', + ].sort(), + ) + }) + + it('isTokenPoolType accepts burn-family + lock-release, rejects others', () => { + assert.equal(isTokenPoolType('BurnMintTokenPool'), true) + assert.equal(isTokenPoolType('BurnFromMintTokenPool'), true) + assert.equal(isTokenPoolType('BurnWithFromMintTokenPool'), true) + assert.equal(isTokenPoolType('LockReleaseTokenPool'), true) + assert.equal(isTokenPoolType('UpgradeableLockReleaseTokenPool'), false) + assert.equal(isTokenPoolType('CCTPThroughCCVTokenPool'), false) + assert.equal(isTokenPoolType('TokenAdminRegistry'), false) + }) + + it('narrows lock-release types with isLockReleaseTokenPoolType, matching the family split', () => { + assert.equal(isLockReleaseTokenPoolType('LockReleaseTokenPool'), true) + assert.equal(isLockReleaseTokenPoolType('SiloedLockReleaseTokenPool'), true) + assert.equal(isLockReleaseTokenPoolType('BurnMintTokenPool'), false) + // the anchored ^Burn rule: a burn pool naming lock-release is still BurnMint + assert.equal(isLockReleaseTokenPoolType('BurnMintWithLockReleaseFlagTokenPool'), false) + // the predicate must agree with getTokenPoolFamily for every supported type + for (const type of TOKEN_POOL_TYPES) + assert.equal(isLockReleaseTokenPoolType(type), getTokenPoolFamily(type) === 'LockRelease') + }) + + it('maps burn-* variants to the BurnMint family, LockRelease to its own', () => { + assert.equal(getTokenPoolFamily('BurnFromMintTokenPool'), 'BurnMint') + assert.equal(getTokenPoolFamily('BurnWithFromMintTokenPool'), 'BurnMint') + assert.equal(getTokenPoolFamily('BurnToAddressTokenPool'), 'BurnMint') + assert.equal(getTokenPoolFamily('BurnMintWithLockReleaseFlagTokenPool'), 'BurnMint') + assert.equal(getTokenPoolFamily('LockReleaseTokenPool'), 'LockRelease') + }) +}) + +describe('pool versions', () => { + it('lists known EVM pool versions low→high', () => { + assert.deepEqual(Object.values(TokenPoolVersion), [ + TokenPoolVersion.V1_5_0, + TokenPoolVersion.V1_5_1, + TokenPoolVersion.V1_6_1, + TokenPoolVersion.V2_0_0, + ]) + }) + + it('isTokenPoolVersion narrows known versions and rejects others', () => { + assert.equal(isTokenPoolVersion(TokenPoolVersion.V1_5_1), true) + assert.equal(isTokenPoolVersion(TokenPoolVersion.V2_0_0), true) + // `1.6.0` is a real on-chain string for SiloedLockReleaseTokenPool (v1.6.0 tag), but its ABI + // isn't in the 2.0.0 dep, so it's deliberately deferred (rejected) — not "no such version". + assert.equal(isTokenPoolVersion('1.6.0'), false) + assert.equal(isTokenPoolVersion('garbage'), false) + }) +}) + +describe('parseTokenPoolVersion', () => { + it('returns { type, version } for a known pool type+version', () => { + assert.deepEqual( + parseTokenPoolVersion({ address: ADDR, contractType: 'BurnMintTokenPool', version: '1.5.1' }), + { + type: 'BurnMintTokenPool', + version: TokenPoolVersion.V1_5_1, + }, + ) + assert.deepEqual( + parseTokenPoolVersion({ + address: ADDR, + contractType: 'LockReleaseTokenPool', + version: '2.0.0', + }), + { + type: 'LockReleaseTokenPool', + version: TokenPoolVersion.V2_0_0, + }, + ) + }) + + it('throws CCTContractTypeInvalidError for an unsupported pool type', () => { + assert.throws( + () => + parseTokenPoolVersion({ + address: ADDR, + contractType: 'TokenAdminRegistry', + version: '1.5.1', + }), + CCTContractTypeInvalidError, + ) + }) + + it('throws CCTContractTypeInvalidError for UpgradeableLockReleaseTokenPool (not in TOKEN_POOL_TYPES)', () => { + assert.throws( + () => + parseTokenPoolVersion({ + address: ADDR, + contractType: 'UpgradeableLockReleaseTokenPool', + version: '1.5.1', + }), + CCTContractTypeInvalidError, + ) + }) + + it('narrows a burn-family variant to its exact type', () => { + assert.deepEqual( + parseTokenPoolVersion({ + address: ADDR, + contractType: 'BurnFromMintTokenPool', + version: '1.5.1', + }), + { type: 'BurnFromMintTokenPool', version: TokenPoolVersion.V1_5_1 }, + ) + }) + + it('throws CCTContractVersionUnsupportedError for an unknown version', () => { + assert.throws( + () => + parseTokenPoolVersion({ + address: ADDR, + contractType: 'BurnMintTokenPool', + version: '1.7.0', + }), + CCTContractVersionUnsupportedError, + ) + }) +}) + +describe('TOKEN_POOL_INTERFACES', () => { + it('provides a cached ethers Interface for each family and version', () => { + for (const family of TOKEN_POOL_FAMILIES) { + for (const version of Object.values(TokenPoolVersion)) { + assert.ok(TOKEN_POOL_INTERFACES[family][version] instanceof Interface) + } + } + }) + + it('resolves distinct Interfaces per family at the same version', () => { + assert.notEqual( + TOKEN_POOL_INTERFACES.BurnMint[TokenPoolVersion.V1_5_1], + TOKEN_POOL_INTERFACES.LockRelease[TokenPoolVersion.V1_5_1], + ) + }) + + it('uses the *_and_proxy variant at V1_5_0 (exposes getPreviousPool)', () => { + assert.ok( + TOKEN_POOL_INTERFACES.BurnMint[TokenPoolVersion.V1_5_0].hasFunction('getPreviousPool'), + ) + assert.ok( + !TOKEN_POOL_INTERFACES.BurnMint[TokenPoolVersion.V1_5_1].hasFunction('getPreviousPool'), + ) + }) +}) + +describe('getTokenPoolInterface', () => { + it('returns the cached family Interface for the type+version (same instance across calls)', () => { + const a = getTokenPoolInterface('BurnMintTokenPool', TokenPoolVersion.V1_5_1) + const b = getTokenPoolInterface('BurnMintTokenPool', TokenPoolVersion.V1_5_1) + assert.ok(a instanceof Interface) + assert.equal(a, b) + assert.equal(a, TOKEN_POOL_INTERFACES.BurnMint[TokenPoolVersion.V1_5_1]) + }) + + it('resolves all burn-* variants to the same BurnMint-family Interface', () => { + const burnMint = getTokenPoolInterface('BurnMintTokenPool', TokenPoolVersion.V1_5_1) + assert.equal(getTokenPoolInterface('BurnFromMintTokenPool', TokenPoolVersion.V1_5_1), burnMint) + assert.equal( + getTokenPoolInterface('BurnWithFromMintTokenPool', TokenPoolVersion.V1_5_1), + burnMint, + ) + assert.equal(getTokenPoolInterface('BurnToAddressTokenPool', TokenPoolVersion.V1_5_1), burnMint) + }) + + it('resolves LockRelease to a different Interface than the BurnMint family', () => { + assert.notEqual( + getTokenPoolInterface('BurnMintTokenPool', TokenPoolVersion.V1_6_1), + getTokenPoolInterface('LockReleaseTokenPool', TokenPoolVersion.V1_6_1), + ) + }) +}) + +describe('resolveEncoder', () => { + it('floor-matches to the encoder at the greatest version ≤ requested', () => { + const encoders = { + [TokenPoolVersion.V1_5_0]: () => 'a', + [TokenPoolVersion.V2_0_0]: () => 'b', + } + assert.equal(resolveEncoder(encoders, TokenPoolVersion.V1_5_0, 'op')(), 'a') + assert.equal(resolveEncoder(encoders, TokenPoolVersion.V1_6_1, 'op')(), 'a') + assert.equal(resolveEncoder(encoders, TokenPoolVersion.V2_0_0, 'op')(), 'b') + }) + + it('throws when nothing is registered at or below the version', () => { + assert.throws( + () => resolveEncoder({ [TokenPoolVersion.V2_0_0]: () => 'b' }, TokenPoolVersion.V1_5_0, 'op'), + CCTOperationUnsupportedError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/contracts.ts b/ccip-sdk/src/cct/evm/token-pool/contracts.ts new file mode 100644 index 00000000..9b906139 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.ts @@ -0,0 +1,218 @@ +/** + * EVM token-pool contract layer for CCT: cached {@link Interface}s + on-chain type/version + * resolution ({@link resolveTokenPool}, {@link getTokenPoolInterface}, floor-matched via + * {@link resolveEncoder}) for read/write ops, plus the deployable pools' creation artifacts + * ({@link getTokenPoolArtifact}). Mirrors `token/contracts.ts`. + * + * @packageDocumentation + */ + +import { Interface } from 'ethers' + +import type { EVMChain } from '../../../evm/index.ts' +import { + CCTContractTypeInvalidError, + CCTContractVersionUnsupportedError, + CCTOperationUnsupportedError, +} from '../../errors.ts' +import BURN_MINT_TOKEN_POOL_V1_5_0_ABI from '../artifacts/abi/V1_5_0/burn-mint-token-pool-and-proxy.ts' +import LOCK_RELEASE_TOKEN_POOL_V1_5_0_ABI from '../artifacts/abi/V1_5_0/lock-release-token-pool-and-proxy.ts' +import BURN_MINT_TOKEN_POOL_V1_5_1_ABI from '../artifacts/abi/V1_5_1/burn-mint-token-pool.ts' +import LOCK_RELEASE_TOKEN_POOL_V1_5_1_ABI from '../artifacts/abi/V1_5_1/lock-release-token-pool.ts' +import BURN_MINT_TOKEN_POOL_V1_6_1_ABI from '../artifacts/abi/V1_6_1/burn-mint-token-pool.ts' +import LOCK_RELEASE_TOKEN_POOL_V1_6_1_ABI from '../artifacts/abi/V1_6_1/lock-release-token-pool.ts' +import BURN_MINT_TOKEN_POOL_V2_0_0_ABI from '../artifacts/abi/V2_0_0/burn-mint-token-pool.ts' +import LOCK_RELEASE_TOKEN_POOL_V2_0_0_ABI from '../artifacts/abi/V2_0_0/lock-release-token-pool.ts' +import BURN_FROM_MINT_TOKEN_POOL_V2_0_0_BYTECODE from '../artifacts/bytecode/V2_0_0/burn-from-mint-token-pool.ts' +import BURN_MINT_TOKEN_POOL_V2_0_0_BYTECODE from '../artifacts/bytecode/V2_0_0/burn-mint-token-pool.ts' +import BURN_WITH_FROM_MINT_TOKEN_POOL_V2_0_0_BYTECODE from '../artifacts/bytecode/V2_0_0/burn-with-from-mint-token-pool.ts' +import LOCK_RELEASE_TOKEN_POOL_V2_0_0_BYTECODE from '../artifacts/bytecode/V2_0_0/lock-release-token-pool.ts' +import type { DeployArtifact } from '../operation.ts' + +/** + * ABI families for pool resolution. The burn-* variants are interface-compatible for CCT + * ops (identical constructor + `transferOwnership`, shared TokenPool surface), so they share + * the `BurnMint` ABI; `LockRelease` (with its liquidity functions) is distinct. + */ +export const TOKEN_POOL_FAMILIES = ['BurnMint', 'LockRelease'] as const + +/** An ABI family for pool resolution. */ +export type TokenPoolFamily = (typeof TOKEN_POOL_FAMILIES)[number] + +/** + * Supported on-chain `typeAndVersion` pool types. The burn-* variants are interface-compatible + * for CCT ops and share the `BurnMint` ABI (see {@link getTokenPoolFamily}); `LockReleaseTokenPool` + * is distinct. Unsupported values fail in {@link parseTokenPoolVersion}. + */ +export const TOKEN_POOL_TYPES = [ + 'BurnMintTokenPool', + 'BurnFromMintTokenPool', + 'BurnWithFromMintTokenPool', + 'BurnToAddressTokenPool', + 'BurnMintWithLockReleaseFlagTokenPool', + 'LockReleaseTokenPool', + 'SiloedLockReleaseTokenPool', +] as const + +/** A supported EVM token-pool contract type. */ +export type TokenPoolType = (typeof TOKEN_POOL_TYPES)[number] + +/** The burn-* mint pool types, which share the `BurnMint` ABI. */ +export type BurnMintTokenPoolType = Extract + +/** The lock/release pool types, which share the `LockRelease` ABI. */ +export type LockReleaseTokenPoolType = Exclude + +/** Type guard for {@link TOKEN_POOL_TYPES}. */ +export function isTokenPoolType(v: string): v is TokenPoolType { + return (TOKEN_POOL_TYPES as readonly string[]).includes(v) +} + +/** + * Classifies a supported pool type into its ABI {@link TokenPoolFamily} by name: every burn-* + * mint pool shares the `BurnMint` ABI (identical surface for CCT ops — including + * `BurnMintWithLockReleaseFlagTokenPool`, hence the anchored `^Burn`), while the non-burn pools + * (`LockReleaseTokenPool`, `SiloedLockReleaseTokenPool`) use the `LockRelease` ABI. + * {@link TOKEN_POOL_TYPES} is the gate, so only allowlisted, ABI-compatible names reach here. + */ +export function getTokenPoolFamily(type: TokenPoolType): TokenPoolFamily { + return /^Burn/.test(type) ? 'BurnMint' : 'LockRelease' +} + +/** Narrows a pool type to the {@link LockReleaseTokenPoolType}s, per {@link getTokenPoolFamily}. */ +export function isLockReleaseTokenPoolType(type: TokenPoolType): type is LockReleaseTokenPoolType { + return getTokenPoolFamily(type) === 'LockRelease' +} + +/** + * Known pool versions, low to high. Value order drives floor-match in {@link resolveEncoder}. + */ +export const TokenPoolVersion = { + V1_5_0: '1.5.0', + V1_5_1: '1.5.1', + V1_6_1: '1.6.1', + V2_0_0: '2.0.0', +} as const + +/** A known EVM token-pool version. */ +export type TokenPoolVersion = (typeof TokenPoolVersion)[keyof typeof TokenPoolVersion] + +/** Type guard for {@link TokenPoolVersion}. */ +export function isTokenPoolVersion(v: string): v is TokenPoolVersion { + return Object.values(TokenPoolVersion).some((known) => known === v) +} + +/** + * Narrows raw `typeAndVersion` strings to a known {@link TokenPoolType} and + * {@link TokenPoolVersion}. + * @throws {@link CCTContractTypeInvalidError} if `contractType` is not a supported pool type + * @throws {@link CCTContractVersionUnsupportedError} if `version` is not a known pool version + */ +export function parseTokenPoolVersion({ + address, + contractType, + version, +}: { + address: string + contractType: string + version: string +}): { type: TokenPoolType; version: TokenPoolVersion } { + if (!isTokenPoolType(contractType)) + throw new CCTContractTypeInvalidError(address, TOKEN_POOL_TYPES.join(', '), contractType) + if (!isTokenPoolVersion(version)) + throw new CCTContractVersionUnsupportedError(contractType, version, { context: { address } }) + return { type: contractType, version } +} + +/** + * Resolves an on-chain pool's type + version from its `typeAndVersion`, narrowed to a known + * {@link TokenPoolType} and {@link TokenPoolVersion}. + * @throws {@link CCTContractTypeInvalidError} if the reported type is not a supported pool type + * @throws {@link CCTContractVersionUnsupportedError} if the reported version is not a known pool version + */ +export async function resolveTokenPool( + chain: EVMChain, + address: string, +): Promise<{ type: TokenPoolType; version: TokenPoolVersion }> { + const [contractType, version] = await chain.typeAndVersion(address) + return parseTokenPoolVersion({ address, contractType, version }) +} + +/** + * Cached pool {@link Interface}s per {@link TokenPoolFamily} and {@link TokenPoolVersion}, + * built once from the vendored `artifacts/` ABIs (no per-call `new Interface`). `V1_5_0` + * uses the `*_and_proxy` variants — the only form `@chainlink/contracts-ccip` ships at 1.5.0. + */ +export const TOKEN_POOL_INTERFACES: Record> = { + BurnMint: { + [TokenPoolVersion.V1_5_0]: new Interface(BURN_MINT_TOKEN_POOL_V1_5_0_ABI), + [TokenPoolVersion.V1_5_1]: new Interface(BURN_MINT_TOKEN_POOL_V1_5_1_ABI), + [TokenPoolVersion.V1_6_1]: new Interface(BURN_MINT_TOKEN_POOL_V1_6_1_ABI), + [TokenPoolVersion.V2_0_0]: new Interface(BURN_MINT_TOKEN_POOL_V2_0_0_ABI), + }, + LockRelease: { + [TokenPoolVersion.V1_5_0]: new Interface(LOCK_RELEASE_TOKEN_POOL_V1_5_0_ABI), + [TokenPoolVersion.V1_5_1]: new Interface(LOCK_RELEASE_TOKEN_POOL_V1_5_1_ABI), + [TokenPoolVersion.V1_6_1]: new Interface(LOCK_RELEASE_TOKEN_POOL_V1_6_1_ABI), + [TokenPoolVersion.V2_0_0]: new Interface(LOCK_RELEASE_TOKEN_POOL_V2_0_0_ABI), + }, +} + +/** + * Returns the cached pool {@link Interface} for `type` and `version`, selected by the + * type's {@link TokenPoolFamily}. Never throws when both came from + * {@link parseTokenPoolVersion}. + */ +export function getTokenPoolInterface(type: TokenPoolType, version: TokenPoolVersion): Interface { + return TOKEN_POOL_INTERFACES[getTokenPoolFamily(type)][version] +} + +/** + * Creation bytecode per deployable pool type (2.0.0 only — pre-2.0.0 bytecode is not vendored). + * The keys define the deployable set ({@link DeployableTokenPoolType}). The burn-* variants share + * the `BurnMint` constructor ABI but are distinct contracts with distinct bytecode. + */ +const TOKEN_POOL_BYTECODE = { + BurnMintTokenPool: BURN_MINT_TOKEN_POOL_V2_0_0_BYTECODE, + BurnFromMintTokenPool: BURN_FROM_MINT_TOKEN_POOL_V2_0_0_BYTECODE, + BurnWithFromMintTokenPool: BURN_WITH_FROM_MINT_TOKEN_POOL_V2_0_0_BYTECODE, + LockReleaseTokenPool: LOCK_RELEASE_TOKEN_POOL_V2_0_0_BYTECODE, +} satisfies Partial> + +/** A pool contract type that can be deployed (has vendored 2.0.0 creation bytecode). */ +export type DeployableTokenPoolType = keyof typeof TOKEN_POOL_BYTECODE + +/** Type guard for {@link DeployableTokenPoolType} (has vendored 2.0.0 creation bytecode). */ +export function isDeployableTokenPoolType(type: string): type is DeployableTokenPoolType { + return Object.hasOwn(TOKEN_POOL_BYTECODE, type) +} + +/** + * Deploy artifact for a deployable pool `type` (v2.0.0): contract name (= `type`), the cached + * constructor {@link Interface}, and the creation bytecode. + */ +export function getTokenPoolArtifact(type: DeployableTokenPoolType): DeployArtifact { + return { + contract: type, + iface: getTokenPoolInterface(type, TokenPoolVersion.V2_0_0), + bytecode: TOKEN_POOL_BYTECODE[type], + } +} + +/** + * Returns the encoder registered at the greatest version less than or equal to + * `version`. One entry per calldata change covers all higher versions via floor-match. + * @throws {@link CCTOperationUnsupportedError} if nothing is registered at or below `version` + */ +export function resolveEncoder( + encoders: Partial>, + version: TokenPoolVersion, + op: string, +): F { + const versions = Object.values(TokenPoolVersion) + for (let i = versions.indexOf(version); i >= 0; i--) { + const encoder = encoders[versions[i]!] + if (encoder !== undefined) return encoder + } + throw new CCTOperationUnsupportedError(op, version) +} diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-pool.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-pool.test.ts new file mode 100644 index 00000000..21576292 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-pool.test.ts @@ -0,0 +1,301 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ZeroAddress, makeError } from 'ethers' + +import { type DeployTokenPoolParams, DeployTokenPool } from './deploy-token-pool.ts' +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import BURN_FROM_MINT_V2_0_0 from '../../artifacts/bytecode/V2_0_0/burn-from-mint-token-pool.ts' +import BURN_MINT_V2_0_0 from '../../artifacts/bytecode/V2_0_0/burn-mint-token-pool.ts' +import BURN_WITH_FROM_MINT_V2_0_0 from '../../artifacts/bytecode/V2_0_0/burn-with-from-mint-token-pool.ts' +import LOCK_RELEASE_V2_0_0 from '../../artifacts/bytecode/V2_0_0/lock-release-token-pool.ts' + +const SENDER = '0x' + '11'.repeat(20) +const TOKEN = '0x' + '22'.repeat(20) +const RMN_PROXY = '0x' + '33'.repeat(20) +const ROUTER = '0x' + '44'.repeat(20) +const HOOKS = '0x' + '55'.repeat(20) +const LOCKBOX = '0x' + '66'.repeat(20) +const DEPLOYED = '0x' + '77'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +const COMMON = { token: TOKEN, localTokenDecimals: 18, rmnProxy: RMN_PROXY, router: ROUTER } + +// Word encodings (32-byte, hex) reused across the golden vectors below. +const W_TOKEN = '0000000000000000000000002222222222222222222222222222222222222222' +const W_DECIMALS = '0000000000000000000000000000000000000000000000000000000000000012' +const W_RMN = '0000000000000000000000003333333333333333333333333333333333333333' +const W_ROUTER = '0000000000000000000000004444444444444444444444444444444444444444' +const W_HOOKS = '0000000000000000000000005555555555555555555555555555555555555555' +const W_LOCKBOX = '0000000000000000000000006666666666666666666666666666666666666666' + +// Golden vectors: pinned 2.0.0 constructor-arg encodings for the fixed inputs above. Independent +// of the SDK encoder — they guard each pool's init-code against drift. The burn-* variants share +// the `BurnMint` constructor (token, decimals, advancedPoolHooks, rmnProxy, router); LockRelease +// adds `lockbox`. +const BURN_MINT_ARGS = W_TOKEN + W_DECIMALS + W_HOOKS + W_RMN + W_ROUTER +const LOCK_RELEASE_ARGS = W_TOKEN + W_DECIMALS + W_HOOKS + W_RMN + W_ROUTER + W_LOCKBOX + +const CASES: { + label: string + params: DeployTokenPoolParams + bytecode: string + ctorArgs: string +}[] = [ + { + label: 'BurnMintTokenPool', + params: { ...COMMON, type: 'BurnMintTokenPool', advancedPoolHooks: HOOKS }, + bytecode: BURN_MINT_V2_0_0, + ctorArgs: BURN_MINT_ARGS, + }, + { + label: 'BurnFromMintTokenPool', + params: { ...COMMON, type: 'BurnFromMintTokenPool', advancedPoolHooks: HOOKS }, + bytecode: BURN_FROM_MINT_V2_0_0, + ctorArgs: BURN_MINT_ARGS, + }, + { + label: 'BurnWithFromMintTokenPool', + params: { ...COMMON, type: 'BurnWithFromMintTokenPool', advancedPoolHooks: HOOKS }, + bytecode: BURN_WITH_FROM_MINT_V2_0_0, + ctorArgs: BURN_MINT_ARGS, + }, + { + label: 'LockReleaseTokenPool', + params: { + ...COMMON, + type: 'LockReleaseTokenPool', + advancedPoolHooks: HOOKS, + lockbox: LOCKBOX, + }, + bytecode: LOCK_RELEASE_V2_0_0, + ctorArgs: LOCK_RELEASE_ARGS, + }, +] + +/** Minimal EVMChain stub — deployTokenPool's build path ignores it; execute uses only these. */ +function stubChain(): EVMChain { + return { + provider: {} as never, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +/** Fake ethers Signer whose deployment receipt carries `contractAddress`. */ +function fakeSigner(opts: { contractAddress?: string | null; waitError?: Error }) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(SENDER), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => + opts.waitError + ? Promise.reject(opts.waitError) + : Promise.resolve({ + status: 1, + contractAddress: + opts.contractAddress === undefined ? DEPLOYED : opts.contractAddress, + }), + }), + } +} + +describe('DeployTokenPool (cct/evm token-pool operation)', () => { + describe('generate (golden vectors per deployable type)', () => { + for (const { label, params, bytecode, ctorArgs } of CASES) { + it(`builds ${label} as init-code with no \`to\``, async () => { + const unsigned = await new DeployTokenPool().generate(stubChain(), { + ...params, + sender: SENDER, + }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal(tx.to, undefined, 'deployment tx has no `to`') + assert.equal(tx.from, SENDER) + assert.ok(tx.data!.startsWith(bytecode), 'data starts with creation bytecode') + assert.equal(tx.data, bytecode + ctorArgs) + }) + } + + it('defaults advancedPoolHooks to the zero address when omitted', async () => { + const unsigned = await new DeployTokenPool().generate(stubChain(), { + ...COMMON, + type: 'BurnMintTokenPool', + }) + const zeroHooks = W_TOKEN + W_DECIMALS + '0'.repeat(64) + W_RMN + W_ROUTER + assert.equal(unsigned.transactions[0]!.data, BURN_MINT_V2_0_0 + zeroHooks) + }) + + it('omits `from` when no sender is given', async () => { + const unsigned = await new DeployTokenPool().generate(stubChain(), { + ...COMMON, + type: 'BurnMintTokenPool', + advancedPoolHooks: HOOKS, + }) + assert.equal(unsigned.transactions[0]!.from, undefined) + }) + }) + + describe('validation', () => { + const base: DeployTokenPoolParams = { + ...COMMON, + type: 'BurnMintTokenPool', + advancedPoolHooks: HOOKS, + } + + it('rejects an invalid token address', async () => { + await assert.rejects( + () => new DeployTokenPool().generate(stubChain(), { ...base, token: 'nope' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'deployTokenPool' && + err.context.param === 'token', + ) + }) + + it('rejects an invalid router address', async () => { + await assert.rejects( + () => new DeployTokenPool().generate(stubChain(), { ...base, router: 'nope' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'router', + ) + }) + + it('rejects decimals outside 0–255', async () => { + await assert.rejects( + () => new DeployTokenPool().generate(stubChain(), { ...base, localTokenDecimals: 256 }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'localTokenDecimals', + ) + }) + + it('rejects an invalid advancedPoolHooks address', async () => { + await assert.rejects( + () => new DeployTokenPool().generate(stubChain(), { ...base, advancedPoolHooks: 'nope' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'advancedPoolHooks', + ) + }) + + it('rejects an invalid sender', async () => { + await assert.rejects( + () => new DeployTokenPool().generate(stubChain(), { ...base, sender: 'nope' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a non-deployable pool type', async () => { + await assert.rejects( + () => + new DeployTokenPool().generate(stubChain(), { + ...COMMON, + type: 'BurnToAddressTokenPool', + } as unknown as DeployTokenPoolParams), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'type', + ) + }) + + it('rejects the zero address for a LockRelease lockbox', async () => { + await assert.rejects( + () => + new DeployTokenPool().generate(stubChain(), { + ...COMMON, + type: 'LockReleaseTokenPool', + advancedPoolHooks: HOOKS, + lockbox: ZeroAddress, + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'lockbox', + ) + }) + + it('rejects an invalid lockbox address', async () => { + await assert.rejects( + () => + new DeployTokenPool().generate(stubChain(), { + ...COMMON, + type: 'LockReleaseTokenPool', + advancedPoolHooks: HOOKS, + lockbox: 'nope', + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'lockbox', + ) + }) + // `lockbox` on a burn pool is a compile-time error (the DeployTokenPoolParams union), so + // there's no runtime case to test. + }) + + describe('execute', () => { + const params: DeployTokenPoolParams = { + ...COMMON, + type: 'BurnMintTokenPool', + advancedPoolHooks: HOOKS, + } + + it('deploys and returns the tx hash and deployed address', async () => { + const result = await new DeployTokenPool().execute(stubChain(), { + ...params, + wallet: fakeSigner({ contractAddress: DEPLOYED }), + }) + assert.deepEqual(result, { + hash: HASH, + contractAddress: DEPLOYED, + verification: { + contract: 'BurnMintTokenPool', + encodedConstructorArgs: '0x' + BURN_MINT_ARGS, + }, + }) + }) + + for (const { label, params: caseParams, ctorArgs } of CASES) { + it(`carries the verification handle for ${label}`, async () => { + const result = await new DeployTokenPool().execute(stubChain(), { + ...caseParams, + wallet: fakeSigner({ contractAddress: DEPLOYED }), + }) + assert.equal(result.verification.contract, caseParams.type) + assert.equal(result.verification.encodedConstructorArgs, '0x' + ctorArgs) + }) + } + + it('throws CCTTxFailedError when the receipt carries no contract address', async () => { + await assert.rejects( + () => + new DeployTokenPool().execute(stubChain(), { + ...params, + wallet: fakeSigner({ contractAddress: null }), + }), + (err: unknown) => + err instanceof CCTTxFailedError && + err.context.operation === 'deployTokenPool' && + !err.isTransient, + ) + }) + + it('throws CCIPExecTxRevertedError when the deployment reverts on-chain', async () => { + await assert.rejects( + () => + new DeployTokenPool().execute(stubChain(), { + ...params, + wallet: fakeSigner({ waitError: makeError('execution reverted', 'CALL_EXCEPTION') }), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && err.context.operation === 'deployTokenPool', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => new DeployTokenPool().execute(stubChain(), { ...params, wallet: {} }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-pool.ts b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-pool.ts new file mode 100644 index 00000000..d35b16c9 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-pool.ts @@ -0,0 +1,126 @@ +/** + * deployTokenPool — deploys a token pool (`type` selects the contract) via raw init-code at + * v2.0.0. The tx has no `to`; `execute` returns the deployed pool address. Mirrors + * `token/operations/deploy-token.ts`. + * + * @packageDocumentation + */ + +import { type Interface, ZeroAddress } from 'ethers' + +import { CCTParamsInvalidError } from '../../../errors.ts' +import { type DeployArtifact, EVMDeployOperation } from '../../operation.ts' +import { validateAddress, validateNonZeroAddress, validateUint8 } from '../../validate.ts' +import { + type DeployableTokenPoolType, + type TokenPoolFamily, + getTokenPoolArtifact, + getTokenPoolFamily, + isDeployableTokenPoolType, +} from '../contracts.ts' + +/** Deployable pool types + their creation bytecode/artifact live in `../contracts.ts`. */ +export type { DeployableTokenPoolType } + +/** Fields shared by every deployable token pool. */ +interface DeployTokenPoolBaseParams { + /** Address of the token the pool manages. */ + token: string + /** The token's `decimals` (uint8). */ + localTokenDecimals: number + /** RMN proxy address. */ + rmnProxy: string + /** CCIP router address. */ + router: string + /** Advanced pool hooks; defaults to the zero address. */ + advancedPoolHooks?: string + /** Deployer address; sets `tx.from` for offline / multisig signing. */ + sender?: string +} + +/** Params for a burn-* mint pool — the burn family shares one constructor shape. */ +export interface DeployBurnMintTokenPoolParams extends DeployTokenPoolBaseParams { + type: Exclude +} + +/** + * Params for a `LockReleaseTokenPool` — the burn constructor plus `lockbox`. + * + * @remarks `lockbox` must be a pre-deployed `ERC20LockBox` for the *same* `token` (the constructor + * calls `lockbox.isTokenSupported(token)`). Sequence: deployToken → deployLockbox → deployTokenPool + * (this) → authorizeLockboxCallers (`addedCallers: [pool]`) → setPool → configure lanes. + */ +export interface DeployLockReleaseTokenPoolParams extends DeployTokenPoolBaseParams { + type: 'LockReleaseTokenPool' + /** Lockbox address; required and must be non-zero — the v2.0.0 constructor reverts on the zero address. */ + lockbox: string +} + +/** + * Parameters for {@link DeployTokenPool}, discriminated on `type`: the burn-* variants share one + * constructor; `LockReleaseTokenPool` additionally requires `lockbox` (a compile-time guarantee). + */ +export type DeployTokenPoolParams = DeployBurnMintTokenPoolParams | DeployLockReleaseTokenPoolParams + +/** Encodes a v2.0.0 pool constructor into init-code args for a given ABI family. */ +type TokenPoolConstructorEncoder = (iface: Interface, p: DeployTokenPoolParams) => string + +/** Burn-* family constructor: `(token, localTokenDecimals, advancedPoolHooks, rmnProxy, router)`. */ +const encodeBurnMintTokenPool: TokenPoolConstructorEncoder = (iface, p) => + iface.encodeDeploy([ + p.token, + p.localTokenDecimals, + p.advancedPoolHooks ?? ZeroAddress, + p.rmnProxy, + p.router, + ]) + +/** LockRelease constructor: the burn-* args plus `lockbox` (only that variant carries it). */ +const encodeLockReleaseTokenPool: TokenPoolConstructorEncoder = (iface, p) => + iface.encodeDeploy([ + p.token, + p.localTokenDecimals, + p.advancedPoolHooks ?? ZeroAddress, + p.rmnProxy, + p.router, + p.type === 'LockReleaseTokenPool' ? p.lockbox : ZeroAddress, + ]) + +/** Deploys a token pool; `execute` resolves to `{ hash, contractAddress, verification }`. */ +export class DeployTokenPool extends EVMDeployOperation { + readonly name = 'deployTokenPool' + + /** Constructor encoder per ABI {@link TokenPoolFamily}; `type` narrows to its family. */ + private readonly encoders: Record = { + BurnMint: encodeBurnMintTokenPool, + LockRelease: encodeLockReleaseTokenPool, + } + + /** Validates the constructor params before building init-code. */ + protected validate(params: DeployTokenPoolParams): void { + if (!isDeployableTokenPoolType(params.type)) + throw new CCTParamsInvalidError( + this.name, + 'type', + `unsupported pool type ${String(params.type)}`, + ) + validateAddress(this.name, 'token', params.token) + validateUint8(this.name, 'localTokenDecimals', params.localTokenDecimals) + validateAddress(this.name, 'rmnProxy', params.rmnProxy) + validateAddress(this.name, 'router', params.router) + if (params.advancedPoolHooks !== undefined) + validateAddress(this.name, 'advancedPoolHooks', params.advancedPoolHooks) + if (params.type === 'LockReleaseTokenPool') + validateNonZeroAddress(this.name, 'lockbox', params.lockbox) + } + + /** Deploy artifact for the selected pool `type` (v2.0.0): name + ctor interface + bytecode. */ + protected artifact(p: DeployTokenPoolParams): DeployArtifact { + return getTokenPoolArtifact(p.type) + } + + /** ABI-encodes the pool constructor args via the encoder for the type's ABI family. */ + protected encode(iface: Interface, p: DeployTokenPoolParams): string { + return this.encoders[getTokenPoolFamily(p.type)](iface, p) + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-state.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-state.test.ts new file mode 100644 index 00000000..9b9ce2a7 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-state.test.ts @@ -0,0 +1,341 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { getAddress, makeError, toBeHex } from 'ethers' + +import { GetTokenPoolState } from './get-token-pool-state.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { parseTypeAndVersion } from '../../../../utils.ts' +import { + CCTContractTypeInvalidError, + CCTContractVersionUnsupportedError, + CCTParamsInvalidError, +} from '../../../errors.ts' +import { type TokenPoolFamily, TOKEN_POOL_INTERFACES, TokenPoolVersion } from '../contracts.ts' + +const POOL = '0x' + '11'.repeat(20) +const TOKEN = '0x' + '22'.repeat(20) +const ROUTER = '0x' + '33'.repeat(20) +const OWNER = '0x' + '44'.repeat(20) +const RMN_PROXY = '0x' + '55'.repeat(20) +const RATE_LIMIT_ADMIN = '0x' + '66'.repeat(20) +const FEE_ADMIN = '0x' + '77'.repeat(20) +const LOCKBOX = '0x' + '99'.repeat(20) + +const CHAINS = [5009297550715157269n, 16015286601757825753n] + +/** Getters the op reads, as `functionName -> return values` (ABI-encoded on demand). */ +type Reads = Record + +/** `getAllowedFinalityConfig` packs the FCR flag above the 16-bit FTF depth, as bytes4. */ +const FINALITY_SAFE_FLAG = 1 << 16 +const finalityConfig = (allowed: number) => toBeHex(allowed, 4) + +/** + * EVMChain stub: `typeAndVersion` reports `typeAndVersion` (parsed the way the real chain does), + * and the provider answers `eth_call` from `reads`, keyed by selector off the pool's own + * Interface. Any getter absent from `reads` reverts. + */ +function stubChain({ + typeAndVersion = 'BurnMintTokenPool 2.0.0', + family = 'BurnMint', + version = TokenPoolVersion.V2_0_0, + reads = {}, + tokenDecimals = 18, +}: { + typeAndVersion?: string + family?: TokenPoolFamily + /** ABI the stub encodes results with — must match the version `typeAndVersion` reports. */ + version?: TokenPoolVersion + reads?: Reads + /** Decimals `getTokenInfo` reports, which pre-v2.0.0 pools read instead of a pool getter. */ + tokenDecimals?: number +} = {}): EVMChain { + const iface = TOKEN_POOL_INTERFACES[family][version] + const responses = new Map( + Object.entries(reads).map(([fn, values]) => [ + iface.getFunction(fn)!.selector, + iface.encodeFunctionResult(fn, values), + ]), + ) + return { + provider: { + call: async ({ data }: { data: string }) => { + const encoded = responses.get(data.slice(0, 10)) + if (!encoded) + throw makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: null, data }, + invocation: null, + revert: null, + }) + return encoded + }, + }, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: () => Promise.resolve(parseTypeAndVersion(typeAndVersion)), + getTokenInfo: () => Promise.resolve({ decimals: tokenDecimals, symbol: 'TKN', name: 'Token' }), + } as unknown as EVMChain +} + +const READS: Reads = { + getToken: [TOKEN], + owner: [OWNER], + getRmnProxy: [RMN_PROXY], + getTokenDecimals: [18], + getSupportedChains: [CHAINS], + getDynamicConfig: [ROUTER, RATE_LIMIT_ADMIN, FEE_ADMIN], + getAllowedFinalityConfig: [finalityConfig(10)], +} + +/** Pre-v2.0.0 getters: router and the rate-limit role stand alone, and there is no fee admin. */ +const LEGACY_READS: Reads = { + getToken: [TOKEN], + owner: [OWNER], + getRouter: [ROUTER], + getRmnProxy: [RMN_PROXY], + getRateLimitAdmin: [RATE_LIMIT_ADMIN], + getSupportedChains: [CHAINS], +} + +describe('GetTokenPoolState (cct/evm token-pool query)', () => { + it('reads a burn-mint pool: token, roles, lanes and allowed finality', async () => { + const state = await new GetTokenPoolState().query(stubChain({ reads: READS }), { + poolAddress: POOL, + }) + + assert.deepEqual(state, { + poolAddress: POOL, + version: '2.0.0', + type: 'BurnMintTokenPool', + token: TOKEN, + tokenDecimals: 18, + router: ROUTER, + owner: OWNER, + rmnProxy: RMN_PROXY, + rateLimitAdmin: RATE_LIMIT_ADMIN, + feeAdmin: FEE_ADMIN, + supportedChains: CHAINS, + finalityDepth: 10, + finalitySafe: false, + }) + }) + + it('reads the lockbox of a lock-release pool', async () => { + const chain = stubChain({ + typeAndVersion: 'LockReleaseTokenPool 2.0.0', + family: 'LockRelease', + reads: { ...READS, getLockBox: [LOCKBOX] }, + }) + + const state = await new GetTokenPoolState().query(chain, { poolAddress: POOL }) + + // narrowing on `version` then `type` is what exposes lockBox — no optional field to check + assert.ok(state.version === '2.0.0' && state.type === 'LockReleaseTokenPool') + assert.equal(state.lockBox, LOCKBOX) + }) + + it('reads router and both admin roles from the single getDynamicConfig call', async () => { + let calls = 0 + const chain = stubChain({ reads: READS }) + const provider = chain.provider as unknown as { + call: (tx: { data: string }) => Promise + } + const { call } = provider + provider.call = (tx) => { + calls++ + return call(tx) + } + + const state = await new GetTokenPoolState().query(chain, { poolAddress: POOL }) + + assert.equal(state.router, ROUTER) + assert.equal(state.rateLimitAdmin, RATE_LIMIT_ADMIN) + assert.ok(state.version === '2.0.0') + assert.equal(state.feeAdmin, FEE_ADMIN) + assert.equal(calls, Object.keys(READS).length, 'one call per getter, none duplicated') + }) + + it('decodes the FCR flag packed above the finality depth', async () => { + const chain = stubChain({ + reads: { ...READS, getAllowedFinalityConfig: [finalityConfig(FINALITY_SAFE_FLAG)] }, + }) + + const state = await new GetTokenPoolState().query(chain, { poolAddress: POOL }) + + assert.ok(state.version === '2.0.0') + assert.equal(state.finalitySafe, true) + assert.equal(state.finalityDepth, 0) + }) + + describe('pre-v2.0.0 pools', () => { + it('reads a v1.5.1 burn-mint pool through the getters that version has', async () => { + const chain = stubChain({ + typeAndVersion: 'BurnMintTokenPool 1.5.1', + version: TokenPoolVersion.V1_5_1, + reads: LEGACY_READS, + }) + + const state = await new GetTokenPoolState().query(chain, { poolAddress: POOL }) + + // no feeAdmin, finality window, or lockbox: none of them exist before v2.0.0 + assert.deepEqual(state, { + poolAddress: POOL, + version: '1.5.1', + type: 'BurnMintTokenPool', + token: TOKEN, + tokenDecimals: 18, + router: ROUTER, + owner: OWNER, + rmnProxy: RMN_PROXY, + rateLimitAdmin: RATE_LIMIT_ADMIN, + supportedChains: CHAINS, + }) + }) + + it('reads a v1.6.1 lock-release pool, which has no lockbox to report', async () => { + const chain = stubChain({ + typeAndVersion: 'LockReleaseTokenPool 1.6.1', + family: 'LockRelease', + version: TokenPoolVersion.V1_6_1, + reads: LEGACY_READS, + }) + + const state = await new GetTokenPoolState().query(chain, { poolAddress: POOL }) + + assert.equal(state.version, '1.6.1') + assert.equal(state.type, 'LockReleaseTokenPool') + // `lockBox` arrives with v2.0.0; narrowing on version is what keeps it off this arm + assert.ok(!('lockBox' in state)) + }) + + it('reads a siloed pool before v2.0.0, where per-lane escrow does not exist yet', async () => { + const chain = stubChain({ + typeAndVersion: 'SiloedLockReleaseTokenPool 1.6.1', + family: 'LockRelease', + version: TokenPoolVersion.V1_6_1, + reads: LEGACY_READS, + }) + + const state = await new GetTokenPoolState().query(chain, { poolAddress: POOL }) + + // only the v2.0.0 reader needs getLockBox(), so there is nothing to reject here + assert.equal(state.type, 'SiloedLockReleaseTokenPool') + assert.equal(state.version, '1.6.1') + }) + + it('takes decimals from the token at v1.5.0, which has no getTokenDecimals', async () => { + const chain = stubChain({ + typeAndVersion: 'BurnMintTokenPool 1.5.0', + version: TokenPoolVersion.V1_5_0, + reads: LEGACY_READS, + tokenDecimals: 6, + }) + + const state = await new GetTokenPoolState().query(chain, { poolAddress: POOL }) + + assert.equal(state.tokenDecimals, 6) + }) + + it('rejects a v1.5.0 AndProxy pool, whose type name is not in the supported set', async () => { + const chain = stubChain({ + typeAndVersion: 'BurnMintTokenPoolAndProxy 1.5.0', + version: TokenPoolVersion.V1_5_0, + reads: LEGACY_READS, + }) + + await assert.rejects( + () => new GetTokenPoolState().query(chain, { poolAddress: POOL }), + (err: unknown) => err instanceof CCTContractTypeInvalidError, + ) + }) + }) + + it('checksums the returned pool address', async () => { + const lowercase = '0x' + 'ab'.repeat(20) + + const state = await new GetTokenPoolState().query(stubChain({ reads: READS }), { + poolAddress: lowercase, + }) + + assert.equal(state.poolAddress, getAddress(lowercase)) + }) + + describe('validation', () => { + it('rejects an invalid pool address before any RPC', async () => { + let probed = false + const chain = stubChain({ reads: READS }) + chain.typeAndVersion = () => { + probed = true + return Promise.resolve(parseTypeAndVersion('BurnMintTokenPool 2.0.0')) + } + + await assert.rejects( + () => new GetTokenPoolState().query(chain, { poolAddress: 'nope' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'getTokenPoolState' && + err.context.param === 'poolAddress', + ) + assert.equal(probed, false, 'validation fails before the typeAndVersion probe') + }) + + it('rejects a pool type outside the supported CCT set', async () => { + const chain = stubChain({ typeAndVersion: 'USDCTokenPoolProxy 2.0.0', reads: READS }) + + await assert.rejects( + () => new GetTokenPoolState().query(chain, { poolAddress: POOL }), + (err: unknown) => err instanceof CCTContractTypeInvalidError, + ) + }) + + it('rejects a siloed pool, whose lockboxes are keyed per remote chain', async () => { + // SiloedLockReleaseTokenPool exposes getLockBox(uint64), not getLockBox() — hence no + // no-arg getter in `reads`: reading it through the LockRelease ABI would hit a selector + // the contract does not implement, so the type has to be rejected up front. + const chain = stubChain({ + typeAndVersion: 'SiloedLockReleaseTokenPool 2.0.0', + family: 'LockRelease', + reads: READS, + }) + + await assert.rejects( + () => new GetTokenPoolState().query(chain, { poolAddress: POOL }), + (err: unknown) => + err instanceof CCTContractTypeInvalidError && + err.context.actual === 'SiloedLockReleaseTokenPool', + ) + }) + + it('tells a siloed pool apart from a wrong address, naming the per-lane getter', async () => { + const chain = stubChain({ + typeAndVersion: 'SiloedLockReleaseTokenPool 2.0.0', + family: 'LockRelease', + reads: READS, + }) + + await assert.rejects( + () => new GetTokenPoolState().query(chain, { poolAddress: POOL }), + (err: unknown) => + err instanceof CCTContractTypeInvalidError && + // the reason, not just the type mismatch — otherwise this reads as "wrong address" + err.message.includes('getLockBox(remoteChainSelector)') && + err.context.reason === (err.message.split(' — ')[1] as string), + ) + }) + + it('rejects a supported pool type reporting a version the SDK does not know', async () => { + const chain = stubChain({ typeAndVersion: 'BurnMintTokenPool 9.9.9', reads: READS }) + + await assert.rejects( + () => new GetTokenPoolState().query(chain, { poolAddress: POOL }), + (err: unknown) => + err instanceof CCTContractVersionUnsupportedError && + err.context.contractType === 'BurnMintTokenPool' && + err.context.version === '9.9.9', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-state.ts b/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-state.ts new file mode 100644 index 00000000..f6f89497 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-state.ts @@ -0,0 +1,286 @@ +/** + * getTokenPoolState — reads a token pool's admin state (v1.5.0–v2.0.0): the owner and admin roles + * CCT writes are gated on, which {@link EVMChain.getTokenPoolConfig} (a transfer-flow read) does + * not return. One reader per version generation, since the getters differ. + * + * @packageDocumentation + */ + +import { getAddress } from 'ethers' +import type { TypedContract } from 'ethers-abitype' + +import type { EVMChain } from '../../../../evm/index.ts' +import { resultToObject } from '../../../../evm/types.ts' +import { decodeFinalityAllowed } from '../../../../extra-args.ts' +import { CCTContractTypeInvalidError } from '../../../errors.ts' +import BURN_MINT_TOKEN_POOL_V1_5_0_ABI from '../../artifacts/abi/V1_5_0/burn-mint-token-pool-and-proxy.ts' +import BURN_MINT_TOKEN_POOL_V2_0_0_ABI from '../../artifacts/abi/V2_0_0/burn-mint-token-pool.ts' +import LOCK_RELEASE_TOKEN_POOL_V2_0_0_ABI from '../../artifacts/abi/V2_0_0/lock-release-token-pool.ts' +import { EVMQuery, getTypedContract } from '../../query.ts' +import { validateAddress } from '../../validate.ts' +import { + type BurnMintTokenPoolType, + type LockReleaseTokenPoolType, + type TokenPoolType, + TokenPoolVersion, + isLockReleaseTokenPoolType, + resolveTokenPool, +} from '../contracts.ts' + +/** Parameters for {@link GetTokenPoolState}. */ +export interface GetTokenPoolStateParams { + /** Token pool contract address to read. */ + poolAddress: string +} + +/** Admin state every supported pool version reports, however each spells the call. */ +type TokenPoolStateCore = { + /** Address read, checksummed. */ + poolAddress: string + /** Token this pool manages. */ + token: string + /** Local decimals of {@link TokenPoolStateCore.token}. */ + tokenDecimals: number + /** Router the pool accepts ramp calls from. */ + router: string + /** Current pool owner — the signer every CCT pool write is gated on. */ + owner: string + /** RMN proxy the pool checks for curses. */ + rmnProxy: string + /** Address that may change rate limits besides the owner. */ + rateLimitAdmin: string + /** Remote chain selectors configured on the pool. */ + supportedChains: bigint[] +} + +/** + * State of a pre-v2.0.0 pool (v1.5.0–v1.6.1), of any supported type: no fee admin, finality + * window, or lockbox, none of which exist before v2.0.0. + * @remarks A legacy pool's `allowList` and (lock/release) `rebalancer` are transfer-flow and + * liquidity concerns, not admin ones; read those via `cct.chain.getTokenPoolConfig()`. + */ +export type LegacyTokenPoolState = TokenPoolStateCore & { + version: Exclude + type: TokenPoolType +} + +/** Admin state v2.0.0 adds to {@link TokenPoolStateCore}: the fee role and the finality window. */ +type TokenPoolStateCoreV2_0_0 = TokenPoolStateCore & { + version: typeof TokenPoolVersion.V2_0_0 + /** Address that may change token transfer fee config besides the owner. */ + feeAdmin: string + /** Min block confirmations the pool allows for Faster-Than-Finality; `0` when FTF is off. */ + finalityDepth: number + /** Whether the pool allows "safe" finality (FCR). */ + finalitySafe: boolean +} + +/** State of a v2.0.0 burn-* mint pool, which mints/burns the token directly. */ +export type BurnMintTokenPoolStateV2_0_0 = TokenPoolStateCoreV2_0_0 & { + type: BurnMintTokenPoolType +} + +/** State of a v2.0.0 lock/release pool, whose liquidity is escrowed in a single lockbox. */ +export type LockReleaseTokenPoolStateV2_0_0 = TokenPoolStateCoreV2_0_0 & { + type: 'LockReleaseTokenPool' + /** Lockbox escrowing this pool's liquidity. */ + lockBox: string +} + +/** + * State of a v2.0.0 pool: `type === 'LockReleaseTokenPool'` adds the `lockBox`, the one field the + * two families do not share. + */ +export type TokenPoolStateV2_0_0 = BurnMintTokenPoolStateV2_0_0 | LockReleaseTokenPoolStateV2_0_0 + +/** + * Admin state of a token pool: `version === '2.0.0'` gates the roles and finality window that + * version added, and `type === 'LockReleaseTokenPool'` gates its `lockBox`. + */ +export type GetTokenPoolStateResult = LegacyTokenPoolState | TokenPoolStateV2_0_0 + +/** The pre-v2.0.0 getters every legacy version declares in both families. */ +type LegacyTokenPoolGetters = Pick< + TypedContract, + 'getToken' | 'owner' | 'getRouter' | 'getRmnProxy' | 'getRateLimitAdmin' | 'getSupportedChains' +> + +/** + * The v2.0.0 getters both families declare identically: a lock/release handle satisfies this too, + * while `getLockBox` stays out of reach of {@link readTokenPoolV2_0_0}. + */ +type TokenPoolGettersV2_0_0 = Pick< + TypedContract, + | 'getToken' + | 'owner' + | 'getRmnProxy' + | 'getTokenDecimals' + | 'getSupportedChains' + | 'getDynamicConfig' + | 'getAllowedFinalityConfig' +> + +/** + * Reads a pre-v2.0.0 pool, where `router` and the rate-limit role have their own getters. + * @remarks v1.5.0 has no `getTokenDecimals`, so decimals come from the token — the one source + * every legacy version shares. + */ +async function readLegacyTokenPool( + chain: EVMChain, + poolAddress: string, + type: TokenPoolType, + version: LegacyTokenPoolState['version'], +): Promise { + const pool: LegacyTokenPoolGetters = getTypedContract( + chain, + poolAddress, + BURN_MINT_TOKEN_POOL_V1_5_0_ABI, + ) + + const [token, owner, router, rmnProxy, rateLimitAdmin, supportedChains] = await Promise.all([ + resultToObject(pool.getToken()), + resultToObject(pool.owner()), + resultToObject(pool.getRouter()), + resultToObject(pool.getRmnProxy()), + resultToObject(pool.getRateLimitAdmin()), + pool.getSupportedChains(), + ]) + const { decimals } = await chain.getTokenInfo(token) + + return { + poolAddress: getAddress(poolAddress), + version, + type, + token, + tokenDecimals: decimals, + router, + owner, + rmnProxy, + rateLimitAdmin, + supportedChains: [...supportedChains], + } +} + +/** Reads the v2.0.0 getters both families share, leaving each caller to add its own type field. */ +async function readTokenPoolV2_0_0( + pool: TokenPoolGettersV2_0_0, + poolAddress: string, +): Promise { + const [token, owner, rmnProxy, tokenDecimals, supportedChains, dynamicConfig, allowedFinality] = + await Promise.all([ + resultToObject(pool.getToken()), + resultToObject(pool.owner()), + resultToObject(pool.getRmnProxy()), + pool.getTokenDecimals(), + pool.getSupportedChains(), + // left raw: `resultToObject` turns a named Result into an object, breaking this destructure + pool.getDynamicConfig(), + pool.getAllowedFinalityConfig(), + ]) + const [router, rateLimitAdmin, feeAdmin] = dynamicConfig + // `allowedFinality` is a bytes4 packing the FCR flag above the 16-bit FTF depth + const { finalityDepth, finalitySafe } = decodeFinalityAllowed(allowedFinality) + + return { + poolAddress: getAddress(poolAddress), + version: TokenPoolVersion.V2_0_0, + token, + owner, + rmnProxy, + router: resultToObject(router), + rateLimitAdmin: resultToObject(rateLimitAdmin), + feeAdmin: resultToObject(feeAdmin), + // ethers decodes every integer type as bigint, including this `uint8` + tokenDecimals: Number(tokenDecimals), + supportedChains: [...supportedChains], + finalityDepth, + finalitySafe: !!finalitySafe, + } +} + +/** Reads a v2.0.0 burn-* mint pool: the shared state, with no escrow to report. */ +async function readBurnMintTokenPoolV2_0_0( + chain: EVMChain, + poolAddress: string, + type: BurnMintTokenPoolType, +): Promise { + const pool = getTypedContract(chain, poolAddress, BURN_MINT_TOKEN_POOL_V2_0_0_ABI) + return { ...(await readTokenPoolV2_0_0(pool, poolAddress)), type } +} + +/** + * Reads a v2.0.0 lock/release pool: the shared state plus the lockbox escrowing its liquidity. + * @throws {@link CCTContractTypeInvalidError} for a siloed pool — it escrows per remote chain + * (`getLockBox(uint64)`), so no single `lockBox` describes it + */ +async function readLockReleaseTokenPoolV2_0_0( + chain: EVMChain, + poolAddress: string, + type: LockReleaseTokenPoolType, +): Promise { + if (type !== 'LockReleaseTokenPool') + throw new CCTContractTypeInvalidError( + poolAddress, + 'LockReleaseTokenPool', + type, + 'siloed pools escrow per remote chain; read per-lane lockboxes via getLockBox(remoteChainSelector)', + ) + + const pool = getTypedContract(chain, poolAddress, LOCK_RELEASE_TOKEN_POOL_V2_0_0_ABI) + const [state, lockBox] = await Promise.all([ + readTokenPoolV2_0_0(pool, poolAddress), + resultToObject(pool.getLockBox()), + ]) + return { ...state, type, lockBox } +} + +/** + * Reads a token pool's admin state — `owner`, the rate-limit role, token/router/lanes, plus + * v2.0.0's `feeAdmin`, finality window and `lockBox` — through the vendored ABI of the pool's + * own version. + */ +export class GetTokenPoolState extends EVMQuery { + readonly name = 'getTokenPoolState' + + /** Validates the pool address; nothing to convert for {@link read}. */ + protected prepare(params: GetTokenPoolStateParams): GetTokenPoolStateParams { + validateAddress(this.name, 'poolAddress', params.poolAddress) + return params + } + + /** + * Resolves the pool's type + version, then reads it through the getters that version has. + * @remarks Dispatch is an exhaustive `switch`, not floor-matched like the write ops' encoders: a + * read's shape is bound to the ABI it decodes through, and the v2.0.0 reader reports its version + * as the literal that discriminates {@link GetTokenPoolStateResult}. Floor-matching would make a + * newer pool misreport itself and silently drop any admin field its version added, so a new + * {@link TokenPoolVersion} fails to compile here until it is pointed at a reader. + * @throws {@link CCTContractTypeInvalidError} if the pool is a v2.0.0 lock/release variant other + * than `LockReleaseTokenPool` — a siloed pool escrows per remote chain (`getLockBox(uint64)`), + * so no single `lockBox` describes it + * @throws {@link CCTContractVersionUnsupportedError} if the reported version is not a known one + */ + protected async read( + chain: EVMChain, + { poolAddress }: GetTokenPoolStateParams, + ): Promise { + const { type, version } = await resolveTokenPool(chain, poolAddress) + + switch (version) { + // pre-v2.0.0 has no lockbox at all, so both families read the same way + case TokenPoolVersion.V1_5_0: + case TokenPoolVersion.V1_5_1: + case TokenPoolVersion.V1_6_1: + return readLegacyTokenPool(chain, poolAddress, type, version) + case TokenPoolVersion.V2_0_0: + return isLockReleaseTokenPoolType(type) + ? readLockReleaseTokenPoolV2_0_0(chain, poolAddress, type) + : readBurnMintTokenPoolV2_0_0(chain, poolAddress, type) + default: { + // a new TokenPoolVersion lands here and fails to compile until it gets a reader + const unread: never = version + return unread + } + } + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts b/ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts new file mode 100644 index 00000000..ae9ce1db --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts @@ -0,0 +1,63 @@ +/** + * transferOwnership: proposes a new TokenPool owner (Ownable2Step; the new + * owner must later call acceptOwnership). + * + * @packageDocumentation + */ + +import type { Interface } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { EVMOperation, callTx } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' +import { + TokenPoolVersion, + getTokenPoolInterface, + resolveEncoder, + resolveTokenPool, +} from '../contracts.ts' + +/** Parameters for {@link TransferOwnership}. */ +export interface TransferOwnershipParams { + poolAddress: string + newOwner: string + /** Current pool owner; sets `tx.from` for offline / multisig signing. */ + sender?: string +} + +/** Encodes `transferOwnership` calldata against the resolved pool {@link Interface}. */ +type Encoder = (iface: Interface, params: TransferOwnershipParams) => UnsignedEVMTx + +const encodeTransferOwnership: Encoder = (iface, { newOwner, poolAddress }) => + callTx(poolAddress, iface.encodeFunctionData('transferOwnership', [newOwner])) + +/** Proposes a new TokenPool owner via Ownable2Step `transferOwnership`. */ +export class TransferOwnership extends EVMOperation { + readonly name = 'transferOwnership' + + /** + * Stable across pool versions: one V1_5_0 entry covers all via floor-match. + * Add another only when a version's encoding diverges. + */ + private readonly encoders: Partial> = { + [TokenPoolVersion.V1_5_0]: encodeTransferOwnership, + } + + /** Validates the pool and new-owner addresses before any RPC. */ + protected validate({ poolAddress, newOwner }: TransferOwnershipParams): void { + validateAddress(this.name, 'poolAddress', poolAddress) + validateAddress(this.name, 'newOwner', newOwner) + } + + /** Reads the pool's type-and-version, then floor-matches the encoder and its contract interface. */ + protected async buildUnsigned( + chain: EVMChain, + { poolAddress, newOwner }: TransferOwnershipParams, + ): Promise { + const { type, version } = await resolveTokenPool(chain, poolAddress) + const iface = getTokenPoolInterface(type, version) + const encode = resolveEncoder(this.encoders, version, this.name) + return encode(iface, { poolAddress, newOwner }) + } +} diff --git a/ccip-sdk/src/cct/evm/token/contracts.ts b/ccip-sdk/src/cct/evm/token/contracts.ts new file mode 100644 index 00000000..cdd3aa18 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/contracts.ts @@ -0,0 +1,69 @@ +/** + * EVM token contract layer for CCT: cached {@link Interface}s per {@link TokenVersion} + * ({@link getTokenInterface}) for read/write (e.g. ownership) ops, and the deployable + * `CrossChainToken` (v2.0.0) artifact ({@link getTokenArtifact}). `2.0.0` is `CrossChainToken`; + * `1.5.1` / `1.6.2` are `FactoryBurnMintERC20`. Mirrors `token-pool/contracts.ts`. + * + * @packageDocumentation + */ + +import { Interface } from 'ethers' + +import { CCTContractVersionUnsupportedError } from '../../errors.ts' +import FACTORY_BURN_MINT_ERC20_V1_5_1_ABI from '../artifacts/abi/V1_5_1/factory-burn-mint-erc20.ts' +import FACTORY_BURN_MINT_ERC20_V1_6_2_ABI from '../artifacts/abi/V1_6_2/factory-burn-mint-erc20.ts' +import CROSS_CHAIN_TOKEN_V2_0_0_ABI from '../artifacts/abi/V2_0_0/cross-chain-token.ts' +import CROSS_CHAIN_TOKEN_V2_0_0_BYTECODE from '../artifacts/bytecode/V2_0_0/cross-chain-token.ts' +import type { DeployArtifact } from '../operation.ts' + +/** + * Known token versions, low to high. `2.0.0` is `CrossChainToken`; `1.5.1` / `1.6.2` + * are `FactoryBurnMintERC20`. + */ +export const TokenVersion = { + V1_5_1: '1.5.1', + V1_6_2: '1.6.2', + V2_0_0: '2.0.0', +} as const + +/** A known token version. */ +export type TokenVersion = (typeof TokenVersion)[keyof typeof TokenVersion] + +/** + * Cached token {@link Interface}s per {@link TokenVersion}, built once from the vendored ABIs + * (no per-call `new Interface`) — for read/write (e.g. ownership) ops. Mirrors + * `TOKEN_POOL_INTERFACES` in `token-pool/contracts.ts`. + */ +export const TOKEN_INTERFACES: Record = { + [TokenVersion.V1_5_1]: new Interface(FACTORY_BURN_MINT_ERC20_V1_5_1_ABI), + [TokenVersion.V1_6_2]: new Interface(FACTORY_BURN_MINT_ERC20_V1_6_2_ABI), + [TokenVersion.V2_0_0]: new Interface(CROSS_CHAIN_TOKEN_V2_0_0_ABI), +} + +/** Returns the cached token {@link Interface} for `version`. */ +export function getTokenInterface(version: TokenVersion): Interface { + return TOKEN_INTERFACES[version] +} + +/** + * Deploy artifacts ({@link DeployArtifact}: contract name + ctor {@link Interface} + creation + * bytecode) keyed by {@link TokenVersion}, built once; read via {@link getTokenArtifact}. Only + * `2.0.0` (`CrossChainToken`) is deployable. + */ +export const TOKEN_ARTIFACTS: Partial> = { + [TokenVersion.V2_0_0]: { + contract: 'CrossChainToken', + iface: TOKEN_INTERFACES[TokenVersion.V2_0_0], + bytecode: CROSS_CHAIN_TOKEN_V2_0_0_BYTECODE, + }, +} + +/** + * Returns the cached deploy artifact for `version`. + * @throws {@link CCTContractVersionUnsupportedError} if `version` has no vendored deploy bytecode + */ +export function getTokenArtifact(version: TokenVersion): DeployArtifact { + const artifact = TOKEN_ARTIFACTS[version] + if (!artifact) throw new CCTContractVersionUnsupportedError('token', version) + return artifact +} diff --git a/ccip-sdk/src/cct/evm/token/operations/deploy-token.test.ts b/ccip-sdk/src/cct/evm/token/operations/deploy-token.test.ts new file mode 100644 index 00000000..f24f6072 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-token.test.ts @@ -0,0 +1,271 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ZeroAddress, makeError } from 'ethers' + +import { DeployToken } from './deploy-token.ts' +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import crossChainBytecode from '../../artifacts/bytecode/V2_0_0/cross-chain-token.ts' + +const SENDER = '0x' + '11'.repeat(20) +const OWNER = '0x' + '11'.repeat(20) +const CCIP_ADMIN = '0x' + '22'.repeat(20) +const ROLE_ADMIN = '0x' + '33'.repeat(20) +const PREMINT_RECIPIENT = '0x' + '44'.repeat(20) +const DEPLOYED = '0x' + '77'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +// Golden vector: a pinned constructor-arg encoding for the fixed inputs below. Independent of +// the SDK encoder — it guards CrossChainToken's init-code (bytecode + constructor) against drift. + +// CrossChainToken ctor: ((name, symbol, maxSupply, preMint, preMintRecipient, decimals, +// ccipAdmin), burnMintRoleAdmin, owner). +const INPUTS = { + name: 'CCIP Test Token', + symbol: 'CCIPT', + decimals: 18, + maxSupply: 0n, + preMint: 1000n, + preMintRecipient: PREMINT_RECIPIENT, + ccipAdmin: CCIP_ADMIN, + burnMintRoleAdmin: ROLE_ADMIN, + owner: OWNER, +} +const CTOR_ARGS = + '0000000000000000000000000000000000000000000000000000000000000060' + + '0000000000000000000000003333333333333333333333333333333333333333' + + '0000000000000000000000001111111111111111111111111111111111111111' + + '00000000000000000000000000000000000000000000000000000000000000e0' + + '0000000000000000000000000000000000000000000000000000000000000120' + + '0000000000000000000000000000000000000000000000000000000000000000' + + '00000000000000000000000000000000000000000000000000000000000003e8' + + '0000000000000000000000004444444444444444444444444444444444444444' + + '0000000000000000000000000000000000000000000000000000000000000012' + + '0000000000000000000000002222222222222222222222222222222222222222' + + '000000000000000000000000000000000000000000000000000000000000000f' + + '43434950205465737420546f6b656e0000000000000000000000000000000000' + + '0000000000000000000000000000000000000000000000000000000000000005' + + '4343495054000000000000000000000000000000000000000000000000000000' +const DEPLOY_DATA = crossChainBytecode + CTOR_ARGS + +/** Minimal EVMChain stub — deployToken's build path ignores it; execute uses only these. */ +function stubChain(): EVMChain { + return { + provider: {} as never, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +/** Fake ethers Signer whose deployment receipt carries `contractAddress`. */ +function fakeSigner(opts: { contractAddress?: string | null; waitError?: Error }) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(SENDER), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => + opts.waitError + ? Promise.reject(opts.waitError) + : Promise.resolve({ + status: 1, + contractAddress: + opts.contractAddress === undefined ? DEPLOYED : opts.contractAddress, + }), + }), + } +} + +describe('DeployToken (cct/evm)', () => { + it('builds a deployment as init-code with no `to` (golden vector)', async () => { + const unsigned = await new DeployToken().generate(stubChain(), { ...INPUTS, sender: SENDER }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal(tx.to, undefined, 'deployment tx has no `to`') + assert.equal(tx.from, SENDER) + assert.ok(tx.data!.startsWith(crossChainBytecode), 'data starts with creation bytecode') + assert.equal(tx.data, DEPLOY_DATA) + }) + + it('omits `from` when no sender is given', async () => { + const unsigned = await new DeployToken().generate(stubChain(), INPUTS) + assert.equal(unsigned.transactions[0]!.from, undefined) + }) + + it('defaults preMint to 0 and a zero preMintRecipient when both omitted', async () => { + const { preMint: _preMint, preMintRecipient: _recipient, ...zeroPreMint } = INPUTS + const unsigned = await new DeployToken().generate(stubChain(), zeroPreMint) + // preMint 0 must pair with the zero address, else CrossChainToken's ctor reverts. + const expected = DEPLOY_DATA.replace( + '00000000000000000000000000000000000000000000000000000000000003e8', + '0'.repeat(64), + ).replace('0000000000000000000000004444444444444444444444444444444444444444', '0'.repeat(64)) + assert.equal(unsigned.transactions[0]!.data, expected) + }) + + it('defaults ccipAdmin/burnMintRoleAdmin to owner when omitted', async () => { + const omitted = await new DeployToken().generate(stubChain(), { + name: 'CCIP Test Token', + symbol: 'CCIPT', + decimals: 18, + maxSupply: 0n, + owner: OWNER, + }) + const explicit = await new DeployToken().generate(stubChain(), { + name: 'CCIP Test Token', + symbol: 'CCIPT', + decimals: 18, + maxSupply: 0n, + ccipAdmin: OWNER, + burnMintRoleAdmin: OWNER, + owner: OWNER, + }) + assert.equal(omitted.transactions[0]!.data, explicit.transactions[0]!.data) + }) + + it('rejects a missing preMintRecipient when preMint > 0', async () => { + const { preMintRecipient: _recipient, ...withoutRecipient } = INPUTS + await assert.rejects( + () => new DeployToken().generate(stubChain(), withoutRecipient), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'preMintRecipient', + ) + }) + + it('rejects a preMintRecipient when preMint is 0', async () => { + await assert.rejects( + () => + new DeployToken().generate(stubChain(), { + ...INPUTS, + preMint: 0n, + preMintRecipient: PREMINT_RECIPIENT, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'preMintRecipient', + ) + }) + + it('rejects a zero-address preMintRecipient when preMint > 0', async () => { + await assert.rejects( + () => new DeployToken().generate(stubChain(), { ...INPUTS, preMintRecipient: ZeroAddress }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'preMintRecipient', + ) + }) + + it('rejects an empty name, tagged with the operation and param', async () => { + await assert.rejects( + () => new DeployToken().generate(stubChain(), { ...INPUTS, name: '' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'deployToken' && + err.context.param === 'name', + ) + }) + + it('rejects decimals outside 0–255', async () => { + await assert.rejects( + () => new DeployToken().generate(stubChain(), { ...INPUTS, decimals: 256 }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'decimals', + ) + }) + + it('rejects a maxSupply above uint256 max', async () => { + await assert.rejects( + () => new DeployToken().generate(stubChain(), { ...INPUTS, maxSupply: 2n ** 256n }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'maxSupply', + ) + }) + + it('rejects an invalid owner', async () => { + await assert.rejects( + () => new DeployToken().generate(stubChain(), { ...INPUTS, owner: 'nope' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'owner', + ) + }) + + it('rejects an invalid ccipAdmin', async () => { + await assert.rejects( + () => new DeployToken().generate(stubChain(), { ...INPUTS, ccipAdmin: 'nope' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'ccipAdmin', + ) + }) + + it('rejects preMint greater than a capped maxSupply', async () => { + await assert.rejects( + () => new DeployToken().generate(stubChain(), { ...INPUTS, maxSupply: 10n, preMint: 11n }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'preMint', + ) + }) + + it('rejects an invalid sender', async () => { + await assert.rejects( + () => new DeployToken().generate(stubChain(), { ...INPUTS, sender: 'nope' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('deploys and returns the tx hash and deployed address', async () => { + const result = await new DeployToken().execute(stubChain(), { + ...INPUTS, + wallet: fakeSigner({ contractAddress: DEPLOYED }), + }) + assert.deepEqual(result, { + hash: HASH, + contractAddress: DEPLOYED, + verification: { contract: 'CrossChainToken', encodedConstructorArgs: '0x' + CTOR_ARGS }, + }) + }) + + it('carries the verification handle recovered from the init-code', async () => { + const result = await new DeployToken().execute(stubChain(), { + ...INPUTS, + wallet: fakeSigner({ contractAddress: DEPLOYED }), + }) + assert.equal(result.verification.contract, 'CrossChainToken') + assert.equal(result.verification.encodedConstructorArgs, '0x' + CTOR_ARGS) + }) + + it('throws CCTTxFailedError when the receipt carries no contract address', async () => { + await assert.rejects( + () => + new DeployToken().execute(stubChain(), { + ...INPUTS, + wallet: fakeSigner({ contractAddress: null }), + }), + (err: unknown) => + err instanceof CCTTxFailedError && + err.context.operation === 'deployToken' && + !err.isTransient, + ) + }) + + it('throws CCIPExecTxRevertedError when the deployment reverts on-chain', async () => { + await assert.rejects( + () => + new DeployToken().execute(stubChain(), { + ...INPUTS, + wallet: fakeSigner({ waitError: makeError('execution reverted', 'CALL_EXCEPTION') }), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && + err.context.operation === 'deployToken' && + err.context.txHash === HASH, + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => new DeployToken().execute(stubChain(), { ...INPUTS, wallet: {} }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts b/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts new file mode 100644 index 00000000..69e2c3d5 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts @@ -0,0 +1,113 @@ +/** + * deployToken — deploys a `CrossChainToken` (v2.0.0) via raw init-code. The tx has no + * `to`; `execute` returns the deployed contract address. + * + * @packageDocumentation + */ + +import { type Interface, ZeroAddress } from 'ethers' + +import { CCTParamsInvalidError } from '../../../errors.ts' +import { type DeployArtifact, EVMDeployOperation } from '../../operation.ts' +import { + validateAddress, + validateNonEmptyString, + validateUint256, + validateUint8, +} from '../../validate.ts' +import { TokenVersion, getTokenArtifact } from '../contracts.ts' + +/** Parameters for {@link DeployToken} — deploys `CrossChainToken` (v2.0.0). */ +export interface DeployTokenParams { + name: string + symbol: string + decimals: number + /** Max supply cap; `0n` means unlimited. */ + maxSupply: bigint + /** Amount minted at deploy; defaults to `0n`. Must be `<= maxSupply` when capped. */ + preMint?: bigint + /** Receives ownership; a valid address. */ + owner: string + /** Recipient of `preMint`; required when `preMint > 0`, must be unset otherwise. */ + preMintRecipient?: string + /** CCIP admin (`getCCIPAdmin`); defaults to `owner`. */ + ccipAdmin?: string + /** Admin of the burn/mint roles; defaults to `owner`. */ + burnMintRoleAdmin?: string + sender?: string +} + +/** Encodes the `CrossChainToken` (v2.0.0) constructor args; admins default to `owner`. */ +function encodeCrossChainToken(iface: Interface, p: DeployTokenParams): string { + return iface.encodeDeploy([ + [ + p.name, + p.symbol, + p.maxSupply, + p.preMint ?? 0n, + // preMintRecipient is set iff preMint > 0 (enforced in validate); zero address otherwise. + p.preMintRecipient ?? ZeroAddress, + p.decimals, + p.ccipAdmin ?? p.owner, + ], + p.burnMintRoleAdmin ?? p.owner, + p.owner, + ]) +} + +/** Deploys a `CrossChainToken`; `execute` resolves to `{ hash, contractAddress, verification }`. */ +export class DeployToken extends EVMDeployOperation { + readonly name = 'deployToken' + + /** Validates the constructor params before building init-code. */ + protected validate(params: DeployTokenParams): void { + validateNonEmptyString(this.name, 'name', params.name) + validateNonEmptyString(this.name, 'symbol', params.symbol) + validateUint8(this.name, 'decimals', params.decimals) + validateUint256(this.name, 'maxSupply', params.maxSupply) + const preMint = params.preMint ?? 0n + validateUint256(this.name, 'preMint', preMint) + validateAddress(this.name, 'owner', params.owner) + if (params.maxSupply !== 0n && preMint > params.maxSupply) + throw new CCTParamsInvalidError( + this.name, + 'preMint', + `must be <= maxSupply (${params.maxSupply}), got ${preMint}`, + ) + // Mirror CrossChainToken's ctor: preMintRecipient is set (and non-zero) iff preMint > 0. + if (preMint > 0n) { + if (params.preMintRecipient === undefined) + throw new CCTParamsInvalidError( + this.name, + 'preMintRecipient', + 'must be set when preMint > 0', + ) + validateAddress(this.name, 'preMintRecipient', params.preMintRecipient) + if (params.preMintRecipient === ZeroAddress) + throw new CCTParamsInvalidError( + this.name, + 'preMintRecipient', + 'must be non-zero when preMint > 0', + ) + } else if (params.preMintRecipient !== undefined) { + throw new CCTParamsInvalidError( + this.name, + 'preMintRecipient', + 'must be unset when preMint is 0', + ) + } + if (params.ccipAdmin !== undefined) validateAddress(this.name, 'ccipAdmin', params.ccipAdmin) + if (params.burnMintRoleAdmin !== undefined) + validateAddress(this.name, 'burnMintRoleAdmin', params.burnMintRoleAdmin) + } + + /** Deploy artifact for `CrossChainToken` (v2.0.0). */ + protected artifact(): DeployArtifact { + return getTokenArtifact(TokenVersion.V2_0_0) + } + + /** ABI-encodes the `CrossChainToken` (v2.0.0) constructor args. */ + protected encode(iface: Interface, params: DeployTokenParams): string { + return encodeCrossChainToken(iface, params) + } +} diff --git a/ccip-sdk/src/cct/evm/validate.ts b/ccip-sdk/src/cct/evm/validate.ts new file mode 100644 index 00000000..5740169a --- /dev/null +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -0,0 +1,88 @@ +/** + * Shared parameter validators for EVM CCT operations. Throws + * {@link CCTParamsInvalidError} before any RPC so invalid inputs fail fast. + * + * @packageDocumentation + */ + +import { ZeroAddress, getAddress, isAddress } from 'ethers' + +import { CCIPAddressInvalidError } from '../../errors/index.ts' +import { ChainFamily } from '../../networks.ts' +import { CCTParamsInvalidError } from '../errors.ts' + +/** + * Asserts `value` is a valid EVM address, narrowing it to `string` for callers. Links the + * canonical {@link CCIPAddressInvalidError} as the `cause`, keeping the + * {@link operation}/{@link param} context on top. + * @throws {@link CCTParamsInvalidError} if `value` is not a valid address + */ +export function validateAddress( + operation: string, + param: string, + value: unknown, +): asserts value is string { + if (typeof value === 'string' && isAddress(value)) return + throw new CCTParamsInvalidError( + operation, + param, + `must be a valid address, got ${String(value)}`, + { + cause: new CCIPAddressInvalidError(String(value), ChainFamily.EVM), + }, + ) +} + +/** + * Asserts `value` is a valid, non-zero EVM address. + * @remarks Normalises with `getAddress` first: a literal `=== ZeroAddress` misses the ICAP + * spelling, and a tx to `0x0` hits no code, so it mines as a successful no-op. + * @throws {@link CCTParamsInvalidError} if `value` is not a valid address, or is the zero address + */ +export function validateNonZeroAddress(operation: string, param: string, value: unknown): void { + validateAddress(operation, param, value) + if (getAddress(value) === ZeroAddress) + throw new CCTParamsInvalidError(operation, param, 'must not be the zero address') +} + +/** + * Asserts `value` is a non-empty (non-blank) string. + * @throws {@link CCTParamsInvalidError} if `value` is not a non-empty string + */ +export function validateNonEmptyString(operation: string, param: string, value: unknown): void { + if (typeof value === 'string' && value.trim().length > 0) return + throw new CCTParamsInvalidError( + operation, + param, + `must be a non-empty string, got ${String(value)}`, + ) +} + +/** + * Asserts `value` is an integer in `[0, 255]` (a Solidity `uint8`). + * @throws {@link CCTParamsInvalidError} if `value` is not such an integer + */ +export function validateUint8(operation: string, param: string, value: unknown): void { + if (typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 255) return + throw new CCTParamsInvalidError( + operation, + param, + `must be an integer in [0, 255], got ${String(value)}`, + ) +} + +/** Largest value representable by a Solidity `uint256`. */ +const UINT256_MAX = BigInt(2) ** BigInt(256) - 1n + +/** + * Asserts `value` is a `bigint` in `[0, 2^256 − 1]` (a Solidity `uint256`). + * @throws {@link CCTParamsInvalidError} if `value` is not such a bigint + */ +export function validateUint256(operation: string, param: string, value: unknown): void { + if (typeof value === 'bigint' && value >= 0n && value <= UINT256_MAX) return + throw new CCTParamsInvalidError( + operation, + param, + `must be a bigint in [0, 2^256 − 1], got ${String(value)}`, + ) +} diff --git a/ccip-sdk/src/cct/operation.ts b/ccip-sdk/src/cct/operation.ts new file mode 100644 index 00000000..215b53ea --- /dev/null +++ b/ccip-sdk/src/cct/operation.ts @@ -0,0 +1,32 @@ +/** + * Cross-family CCT write contract. {@link Operation} defines the shared + * generate/execute surface; each chain family supplies its own lifecycle base. + * + * @packageDocumentation + */ + +import type { ChainTransaction } from '../types.ts' + +/** Result of a successful CCT write: the confirmed on-chain tx hash. */ +export type TransactionResult = Pick + +/** + * Execute params for a CCT write: an op's own params plus the signing `wallet`. + * Families extend with submit-time extras (e.g. Solana's `computeUnits`). + */ +export type ExecuteParams

= P & { wallet: unknown } + +/** + * Abstract CCT write operation: build unsigned tx(s) with {@link generate}, or + * sign and submit with {@link execute}. + */ +export abstract class Operation { + /** camelCase id; matches the token-manager facade method and error context. */ + abstract readonly name: string + /** Reject invalid params before any chain RPC. */ + protected abstract validate(params: Params): void + /** Build unsigned transaction(s); no wallet required. */ + abstract generate(chain: Chain, params: Params): Promise + /** Sign and submit via `params.wallet`; returns once confirmed. */ + abstract execute(chain: Chain, params: ExecuteParams): Promise +} diff --git a/ccip-sdk/src/cct/query.ts b/ccip-sdk/src/cct/query.ts new file mode 100644 index 00000000..4bf275b0 --- /dev/null +++ b/ccip-sdk/src/cct/query.ts @@ -0,0 +1,28 @@ +/** + * Cross-family CCT read contract: {@link Query} wires prepare → read, the read-only counterpart + * of `cct/operation.ts`. Each chain family binds it to its own `Chain` type. + * + * @packageDocumentation + */ + +/** + * Abstract CCT read base. Subclasses supply {@link prepare} and {@link read}; no wallet, no + * calldata, no submit. + * @remarks Validation lives in `prepare`, not a hook of its own: a parser that converts an address + * validates it on the way through, so splitting them would check the same field twice. + */ +export abstract class Query { + /** camelCase id; matches the token-manager facade method and error context. */ + abstract readonly name: string + + /** Validate and normalize params before any chain RPC, without mutating the caller's input. */ + protected abstract prepare(params: Params): Parsed + + /** Read and normalize chain state; runs only after {@link prepare} passes. */ + protected abstract read(chain: Chain, params: Parsed): Promise + + /** Run {@link prepare} and {@link read}; no wallet. */ + async query(chain: Chain, params: Params): Promise { + return this.read(chain, this.prepare(params)) + } +} diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts new file mode 100644 index 00000000..f4c454a6 --- /dev/null +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -0,0 +1,112 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Connection } from '@solana/web3.js' + +import { SolanaTokenManager } from './index.ts' +import type { + GetTokenPoolStateParams, + GetTokenPoolStateResult, +} from './token-pool/operations/index.ts' +import { SolanaChain } from '../../solana/index.ts' + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +describe('SolanaTokenManager (cct/solana)', () => { + it('fromChain exposes flat Solana CCT operations', () => { + const chain = stubChain() + const cct = SolanaTokenManager.fromChain(chain) + assert.equal(cct.chain, chain) + assert.equal(cct.provider, chain.connection) + // Token operations + assert.equal(typeof cct.generateUnsignedDeployToken, 'function') + assert.equal(typeof cct.deployToken, 'function') + assert.equal(typeof cct.generateUnsignedCreateTokenAccount, 'function') + assert.equal(typeof cct.createTokenAccount, 'function') + + // Token admin registry operations + assert.equal(typeof cct.generateUnsignedAcceptAdmin, 'function') + assert.equal(typeof cct.acceptAdmin, 'function') + assert.equal(typeof cct.generateUnsignedCreateLookupTable, 'function') + assert.equal(typeof cct.createLookupTable, 'function') + assert.equal(typeof cct.generateUnsignedAppendToLookupTable, 'function') + assert.equal(typeof cct.appendToLookupTable, 'function') + assert.equal(typeof cct.generateUnsignedRegisterAdmin, 'function') + assert.equal(typeof cct.registerAdmin, 'function') + assert.equal(typeof cct.generateUnsignedSetPool, 'function') + assert.equal(typeof cct.setPool, 'function') + assert.equal(typeof cct.generateUnsignedTransferAdmin, 'function') + assert.equal(typeof cct.transferAdmin, 'function') + assert.equal(typeof cct.getTokenAdminRegistry, 'function') + assert.equal(typeof cct.getSupportedTokens, 'function') + + // Token pool operations + assert.equal(typeof cct.generateUnsignedAppendRemotePoolAddresses, 'function') + assert.equal(typeof cct.appendRemotePoolAddresses, 'function') + assert.equal(typeof cct.generateUnsignedApplyChainUpdates, 'function') + assert.equal(typeof cct.applyChainUpdates, 'function') + assert.equal(typeof cct.generateUnsignedConfigureAllowlist, 'function') + assert.equal(typeof cct.configureAllowlist, 'function') + assert.equal(typeof cct.generateUnsignedCreateTokenMultisig, 'function') + assert.equal(typeof cct.createTokenMultisig, 'function') + assert.equal(typeof cct.generateUnsignedDeployTokenPool, 'function') + assert.equal(typeof cct.deployTokenPool, 'function') + assert.equal(typeof cct.generateUnsignedDeleteChainRemoteConfig, 'function') + assert.equal(typeof cct.deleteChainRemoteConfig, 'function') + assert.equal(typeof cct.generateUnsignedSetChainRateLimit, 'function') + assert.equal(typeof cct.setChainRateLimit, 'function') + assert.equal(typeof cct.generateUnsignedSetRateLimitAdmin, 'function') + assert.equal(typeof cct.setRateLimitAdmin, 'function') + assert.equal(typeof cct.generateUnsignedTransferOwnership, 'function') + assert.equal(typeof cct.transferOwnership, 'function') + assert.equal(typeof cct.generateUnsignedAcceptOwnership, 'function') + assert.equal(typeof cct.acceptOwnership, 'function') + assert.equal(typeof cct.generateUnsignedEditChainRemoteConfig, 'function') + assert.equal(typeof cct.editChainRemoteConfig, 'function') + assert.equal(typeof cct.generateUnsignedRemoveFromAllowlist, 'function') + assert.equal(typeof cct.removeFromAllowlist, 'function') + assert.equal(typeof cct.getTokenPoolRemotes, 'function') + assert.equal(typeof cct.getTokenPoolState, 'function') + }) + + it('creates from a connection provider', async (t) => { + const chain = stubChain() + const connection = new Connection('http://localhost:8899') + t.mock.method(SolanaChain, 'fromConnection', async (provider: Connection) => { + assert.equal(provider, connection) + return chain + }) + + const cct = await SolanaTokenManager.fromProvider(connection) + + assert.equal(cct.chain, chain) + }) + + it('creates from an RPC URL', async (t) => { + const chain = stubChain() + t.mock.method(SolanaChain, 'fromUrl', async (url: string) => { + assert.equal(url, 'http://localhost:8899') + return chain + }) + + const cct = await SolanaTokenManager.fromUrl('http://localhost:8899') + + assert.equal(cct.chain, chain) + }) + + it('getTokenPoolState accepts params whose pool program is not known statically', () => { + const cct = SolanaTokenManager.fromChain(stubChain()) + // A parameter is not narrowed to one PoolProgramRef arm the way a const literal is, so this + // only compiles while a `GetTokenPoolStateParams` overload is declared: TypeScript never + // exposes the implementation signature to callers. + const read = (opts: GetTokenPoolStateParams): Promise => + cct.getTokenPoolState(opts) + + assert.equal(typeof read, 'function') + }) +}) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts new file mode 100644 index 00000000..b1c4aac6 --- /dev/null +++ b/ccip-sdk/src/cct/solana/index.ts @@ -0,0 +1,1678 @@ +/** + * Solana Cross-Chain Token (CCT) admin operations. + * + * @packageDocumentation + */ + +import type { Connection } from '@solana/web3.js' + +import type { ChainContext } from '../../chain.ts' +import type { ChainFamily } from '../../networks.ts' +import type { SolanaChain } from '../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../solana/types.ts' +import { TokenManager } from '../token-manager.ts' +import { type SerializedSolanaTxEncoding, serializeUnsignedSolanaTx } from './serialize.ts' +import { + type ExecuteCreateTokenAccountParams, + type ExecuteCreateTokenAccountResult, + type ExecuteDeployTokenParams, + type ExecuteDeployTokenResult, + type GenerateCreateTokenAccountParams, + type GenerateCreateTokenAccountResult, + type GenerateDeployTokenParams, + type GenerateDeployTokenResult, + CreateTokenAccount, +} from './token/operations/index.ts' +import { + type ExecuteAcceptAdminParams, + type ExecuteAcceptAdminResult, + type ExecuteAppendToLookupTableParams, + type ExecuteAppendToLookupTableResult, + type ExecuteCreateLookupTableParams, + type ExecuteCreateLookupTableResult, + type ExecuteRegisterAdminParams, + type ExecuteRegisterAdminResult, + type ExecuteSetPoolParams, + type ExecuteSetPoolResult, + type ExecuteTransferAdminParams, + type ExecuteTransferAdminResult, + type GenerateAcceptAdminParams, + type GenerateAcceptAdminResult, + type GenerateAppendToLookupTableParams, + type GenerateAppendToLookupTableResult, + type GenerateCreateLookupTableParams, + type GenerateCreateLookupTableResult, + type GenerateRegisterAdminParams, + type GenerateRegisterAdminResult, + type GenerateSetPoolParams, + type GenerateSetPoolResult, + type GenerateTransferAdminParams, + type GenerateTransferAdminResult, + type GetSupportedTokensParams, + type GetTokenAdminRegistryParams, + type GetTokenAdminRegistryResult, + AcceptAdmin, + AppendToLookupTable, + CreateLookupTable, + GetSupportedTokens, + GetTokenAdminRegistry, + RegisterAdmin, + SetPool, + TransferAdmin, +} from './token-admin-registry/operations/index.ts' +import { + type BaseGetTokenPoolStateResult, + type BurnMintPoolProgramRef, + type CustomPoolProgramRef, + type ExecuteAcceptOwnershipParams, + type ExecuteAcceptOwnershipResult, + type ExecuteAppendRemotePoolAddressesParams, + type ExecuteAppendRemotePoolAddressesResult, + type ExecuteApplyChainUpdatesParams, + type ExecuteApplyChainUpdatesResult, + type ExecuteConfigureAllowlistParams, + type ExecuteConfigureAllowlistResult, + type ExecuteCreateTokenMultisigParams, + type ExecuteCreateTokenMultisigResult, + type ExecuteDeleteChainRemoteConfigParams, + type ExecuteDeleteChainRemoteConfigResult, + type ExecuteDeployTokenPoolParams, + type ExecuteDeployTokenPoolResult, + type ExecuteEditChainRemoteConfigParams, + type ExecuteEditChainRemoteConfigResult, + type ExecuteInitChainRemoteConfigParams, + type ExecuteInitChainRemoteConfigResult, + type ExecuteRemoveFromAllowlistParams, + type ExecuteRemoveFromAllowlistResult, + type ExecuteSetChainRateLimitParams, + type ExecuteSetChainRateLimitResult, + type ExecuteSetRateLimitAdminParams, + type ExecuteSetRateLimitAdminResult, + type ExecuteTransferOwnershipParams, + type ExecuteTransferOwnershipResult, + type GenerateAcceptOwnershipParams, + type GenerateAcceptOwnershipResult, + type GenerateAppendRemotePoolAddressesParams, + type GenerateAppendRemotePoolAddressesResult, + type GenerateApplyChainUpdatesParams, + type GenerateApplyChainUpdatesResult, + type GenerateConfigureAllowlistParams, + type GenerateConfigureAllowlistResult, + type GenerateCreateTokenMultisigParams, + type GenerateCreateTokenMultisigResult, + type GenerateDeleteChainRemoteConfigParams, + type GenerateDeleteChainRemoteConfigResult, + type GenerateDeployTokenPoolParams, + type GenerateDeployTokenPoolResult, + type GenerateEditChainRemoteConfigParams, + type GenerateEditChainRemoteConfigResult, + type GenerateInitChainRemoteConfigParams, + type GenerateInitChainRemoteConfigResult, + type GenerateRemoveFromAllowlistParams, + type GenerateRemoveFromAllowlistResult, + type GenerateSetChainRateLimitParams, + type GenerateSetChainRateLimitResult, + type GenerateSetRateLimitAdminParams, + type GenerateSetRateLimitAdminResult, + type GenerateTransferOwnershipParams, + type GenerateTransferOwnershipResult, + type GetTokenPoolRemotesParams, + type GetTokenPoolRemotesResult, + type GetTokenPoolStateParams, + type GetTokenPoolStateResult, + type LockReleaseGetTokenPoolStateResult, + type LockReleasePoolProgramRef, + AcceptOwnership, + AppendRemotePoolAddresses, + ApplyChainUpdates, + ConfigureAllowlist, + CreateTokenMultisig, + DeleteChainRemoteConfig, + DeployTokenPool, + EditChainRemoteConfig, + GetTokenPoolRemotes, + GetTokenPoolState, + InitChainRemoteConfig, + RemoveFromAllowlist, + SetChainRateLimit, + SetRateLimitAdmin, + TransferOwnership, +} from './token-pool/operations/index.ts' + +/** CCT admin facade for Solana. */ +export class SolanaTokenManager extends TokenManager { + readonly chain: SolanaChain + // Token operations + readonly #createTokenAccount = new CreateTokenAccount() + + // Token admin registry operations + readonly #acceptAdmin = new AcceptAdmin() + readonly #appendToLookupTable = new AppendToLookupTable() + readonly #createLookupTable = new CreateLookupTable() + readonly #getSupportedTokens = new GetSupportedTokens() + readonly #getTokenAdminRegistry = new GetTokenAdminRegistry() + readonly #registerAdmin = new RegisterAdmin() + readonly #setPool = new SetPool() + readonly #transferAdmin = new TransferAdmin() + + // Token pool operations + readonly #acceptOwnership = new AcceptOwnership() + readonly #appendRemotePoolAddresses = new AppendRemotePoolAddresses() + readonly #applyChainUpdates = new ApplyChainUpdates() + readonly #configureAllowlist = new ConfigureAllowlist() + readonly #createTokenMultisig = new CreateTokenMultisig() + readonly #deployTokenPool = new DeployTokenPool() + readonly #deleteChainRemoteConfig = new DeleteChainRemoteConfig() + readonly #editChainRemoteConfig = new EditChainRemoteConfig() + readonly #getTokenPoolRemotes = new GetTokenPoolRemotes() + readonly #getTokenPoolState = new GetTokenPoolState() + readonly #initChainRemoteConfig = new InitChainRemoteConfig() + readonly #removeFromAllowlist = new RemoveFromAllowlist() + readonly #setChainRateLimit = new SetChainRateLimit() + readonly #setRateLimitAdmin = new SetRateLimitAdmin() + readonly #transferOwnership = new TransferOwnership() + + /** Creates a Solana CCT manager for an existing chain. */ + constructor(chain: SolanaChain) { + super() + this.chain = chain + } + + /** Wraps an existing {@link SolanaChain}. */ + static fromChain(chain: SolanaChain): SolanaTokenManager { + return new SolanaTokenManager(chain) + } + + /** Creates from a Solana web3.js connection. */ + static async fromProvider(provider: Connection, ctx?: ChainContext): Promise { + const { SolanaChain } = await import('../../solana/index.ts') + return new SolanaTokenManager(await SolanaChain.fromConnection(provider, ctx)) + } + + /** Creates from an RPC URL. */ + static async fromUrl(url: string, ctx?: ChainContext): Promise { + const { SolanaChain } = await import('../../solana/index.ts') + return new SolanaTokenManager(await SolanaChain.fromUrl(url, ctx)) + } + + /** Provider of the underlying chain. */ + get provider(): Connection { + return this.chain.connection + } + + /** + * Builds unsigned Solana mint creation instructions, optionally with initial supply. + * The `payer` defaults as mint, freeze, and metadata update authority. + * + * @throws {@link CCTParamsInvalidError} If token parameters are invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedDeployToken({ + * payer, + * decimals: 9, + * tokenProgram: 'spl-token', + * withMetaplex: true, + * name: 'My Token', + * symbol: 'MTK', + * }) + * ``` + */ + async generateUnsignedDeployToken( + opts: GenerateDeployTokenParams, + ): Promise { + const { DeployToken } = await import('./token/operations/index.ts') + return new DeployToken().generate(this.chain, opts) + } + + /** + * Creates a Solana mint, optionally with initial supply. + * The wallet public key defaults as mint, freeze, and metadata update authority. + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If token parameters are invalid. + * @throws {@link CCTTxFailedError} If transaction simulation or submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.deployToken({ + * wallet, + * decimals: 9, + * tokenProgram: 'spl-token', + * withMetaplex: false, + * }) + * ``` + */ + async deployToken(opts: ExecuteDeployTokenParams): Promise { + const { DeployToken } = await import('./token/operations/index.ts') + return new DeployToken().execute(this.chain, opts) + } + + /** + * Builds an unsigned idempotent associated token account create instruction. + * + * @remarks + * This operation is idempotent and safe to re-run. For the canonical pool setup flow, pass the + * `poolSignerAddress` returned by `generateUnsignedDeployTokenPool` as `ownerAddress`, then call + * `generateUnsignedSetPool`. + * + * @see {@link generateUnsignedDeployTokenPool} + * @see {@link generateUnsignedSetPool} + * + * @throws {@link CCTParamsInvalidError} If an address is invalid. + * @throws {@link CCIPTokenMintNotFoundError} If the mint does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedCreateTokenAccount({ + * payer, + * tokenAddress: mint, + * ownerAddress: owner, + * }) + * ``` + */ + generateUnsignedCreateTokenAccount( + opts: GenerateCreateTokenAccountParams, + ): Promise { + return this.#createTokenAccount.generate(this.chain, opts) + } + + /** + * Creates an associated token account for a wallet or PDA owner. + * + * @remarks + * This operation is idempotent and safe to re-run. For the canonical pool setup flow, pass the + * `poolSignerAddress` returned by `deployTokenPool` as `ownerAddress`, then call `setPool`. + * + * @see {@link deployTokenPool} + * @see {@link setPool} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address is invalid. + * @throws {@link CCIPTokenMintNotFoundError} If the mint does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * @throws {@link CCTTxFailedError} If transaction simulation or submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.createTokenAccount({ wallet, tokenAddress: mint, ownerAddress: owner }) + * ``` + */ + createTokenAccount( + opts: ExecuteCreateTokenAccountParams, + ): Promise { + return this.#createTokenAccount.execute(this.chain, opts) + } + + /** + * Builds unsigned SPL Token multisig creation instructions. + * The pool signer PDA occupies `threshold` slots; non-pool signers must meet the threshold independently. + * + * @remarks When `payer` differs from the mint authority, both must sign: the mint authority is + * the `createAccountWithSeed` base account. + * + * @throws {@link CCTParamsInvalidError} If multisig parameters are invalid or the mint has no authority. + * @throws {@link CCIPTokenMintNotFoundError} If the mint does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedCreateTokenMultisig({ + * payer, + * tokenAddress: mint, + * poolType: 'burn-mint', + * threshold: 2, + * additionalSigners: [admin], + * }) + * ``` + */ + generateUnsignedCreateTokenMultisig( + opts: GenerateCreateTokenMultisigParams, + ): Promise { + return this.#createTokenMultisig.generate(this.chain, opts) + } + + /** + * Creates an SPL Token multisig account. + * The pool signer PDA occupies `threshold` slots; non-pool signers must meet the threshold independently. + * Wallet pays fees and must match the mint authority. + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If multisig parameters are invalid or the wallet is not the mint authority. + * @throws {@link CCIPTokenMintNotFoundError} If the mint does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * @throws {@link CCTTxFailedError} If transaction simulation or submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const { hash, multisigAddress } = await cct.createTokenMultisig({ + * wallet, + * tokenAddress: mint, + * poolType: 'burn-mint', + * threshold: 2, + * additionalSigners: [admin], + * }) + * ``` + */ + createTokenMultisig( + opts: ExecuteCreateTokenMultisigParams, + ): Promise { + return this.#createTokenMultisig.execute(this.chain, opts) + } + + /** + * Builds unsigned Solana pool lookup table instructions. + * + * Defaults to create+extend. Specify a canonical `poolType` or custom `poolProgramAddress`. + * Use `mode: 'createEmpty'` to create an empty ALT, e.g. with an EOA payer and vault authority, + * then populate it later through the authority. If `authority` is omitted, it defaults to `payer`. + * + * @throws {@link CCTParamsInvalidError} If an address or lookup table parameter is invalid. + * @throws {@link CCIPTokenMintNotFoundError} If the mint does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedCreateLookupTable({ + * mode: 'createEmpty', + * payer: eoa, + * authority: squadsVault, + * }) + * ``` + */ + generateUnsignedCreateLookupTable( + opts: GenerateCreateLookupTableParams, + ): Promise { + return this.#createLookupTable.generate(this.chain, opts) + } + + /** + * Creates a Solana pool lookup table. Defaults to create+extend; pass `mode: 'createEmpty'` to + * create an empty ALT owned by `authority` and paid by `wallet`. If `authority` is omitted, it + * defaults to the wallet public key. + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address or lookup table parameter is invalid. + * @throws {@link CCIPTokenMintNotFoundError} If the mint does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * @throws {@link CCTTxFailedError} If transaction simulation or submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const { hash, lookupTableAddress } = await cct.createLookupTable({ + * mode: 'createEmpty', + * authority: squadsVault, + * wallet, + * }) + * ``` + */ + createLookupTable(opts: ExecuteCreateLookupTableParams): Promise { + return this.#createLookupTable.execute(this.chain, opts) + } + + /** + * Builds an unsigned instruction to append addresses to a token pool allowlist and toggle + * enforcement. Every call overwrites enforcement; pass `add: []` to toggle it without appending + * an address. Addresses in `add` must be unique; existing allowlist entries are rejected by the + * program. The pool must be initialized first. Pass canonical `poolType` or a compatible + * `poolProgramAddress`; `authority` defaults to `payer`. + * + * @see {@link configureAllowlist} + * @see {@link generateUnsignedDeployTokenPool} + * @see {@link generateUnsignedRemoveFromAllowlist} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedConfigureAllowlist({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * add: [allowedSender], + * enabled: true, + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedConfigureAllowlist( + opts: GenerateConfigureAllowlistParams, + ): Promise { + return this.#configureAllowlist.generate(this.chain, opts) + } + + /** + * Appends addresses to and configures an initialized Solana token pool allowlist using the pool + * owner wallet. Every call overwrites enforcement; pass `add: []` to toggle it without + * appending an address. Addresses in `add` must be unique; existing allowlist entries are + * rejected by the program. + * + * @see {@link generateUnsignedConfigureAllowlist} + * @see {@link deployTokenPool} + * @see {@link removeFromAllowlist} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid or the authority differs + * from the executing wallet. + * @throws {@link CCTTxFailedError} If transaction simulation or submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.configureAllowlist({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * add: [], + * enabled: false, + * wallet, + * }) + * ``` + */ + configureAllowlist( + opts: ExecuteConfigureAllowlistParams, + ): Promise { + return this.#configureAllowlist.execute(this.chain, opts) + } + + /** + * Builds unsigned Solana token pool initialize instructions. + * + * @remarks + * This only builds the pool `initialize` instruction for the canonical `burn-mint` and + * `lock-release` programs selected by `poolType`; custom pool deployment is unsupported. `authority` + * must be allowed to initialize the pool. This does not create the pool signer PDA's associated + * token account; use the returned `poolSignerAddress` with `generateUnsignedCreateTokenAccount` + * before `generateUnsignedSetPool`. + * + * @see {@link generateUnsignedCreateTokenAccount} + * @see {@link generateUnsignedSetPool} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedDeployTokenPool({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * payer, + * authority, + * allowlist: [allowedSender], + * }) + * ``` + */ + generateUnsignedDeployTokenPool( + opts: GenerateDeployTokenPoolParams, + ): Promise { + return this.#deployTokenPool.generate(this.chain, opts) + } + + /** + * Initializes a Solana token pool. + * + * @remarks + * This only sends the pool `initialize` instruction for the canonical `burn-mint` and + * `lock-release` programs selected by `poolType`; custom pool deployment is unsupported. The signer + * must be allowed to initialize the pool. This does not create the pool signer PDA's associated + * token account; use the returned `poolSignerAddress` with `createTokenAccount` before `setPool`. + * + * @see {@link createTokenAccount} + * @see {@link setPool} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid. + * @throws {@link CCTTxFailedError} If transaction simulation or submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.deployTokenPool({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * wallet, + * }) + * ``` + */ + deployTokenPool(opts: ExecuteDeployTokenPoolParams): Promise { + return this.#deployTokenPool.execute(this.chain, opts) + } + + /** + * Builds ordered unsigned transactions that remove remote-chain configs and add new configs with + * their remote pools and rate limits. This accepts the same `remoteChainSelectorsToRemove` and + * `chainsToAdd` parameters as EVM `applyChainUpdates`. + * + * @remarks Removals run before additions. Each added chain is initialized, configured, and + * rate-limited as one transaction group; returns one or more packed transactions. To replace a + * chain, include its selector in both `remoteChainSelectorsToRemove` and `chainsToAdd`. + * Solana requires `remoteTokenDecimals`. `authority` must be the pool owner and defaults to `payer`. + * + * @see {@link applyChainUpdates} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter or chain update is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsignedTxs = await cct.generateUnsignedApplyChainUpdates({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelectorsToRemove: [oldSelector], + * chainsToAdd: [{ + * remoteChainSelector: newSelector, + * remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + * remotePoolAddresses: ['0x1234567890abcdef1234567890abcdef12345678'], + * remoteTokenDecimals: 18, + * inboundRateLimiterConfig: { enabled: false }, + * outboundRateLimiterConfig: { enabled: true, capacity: 1_000_000n, rate: 1_000n }, + * }], + * payer, + * authority, + * }) + * + * for (const unsignedTx of unsignedTxs) { + * // Sign and submit each transaction in order. + * } + * ``` + */ + generateUnsignedApplyChainUpdates( + opts: GenerateApplyChainUpdatesParams, + ): Promise { + return this.#applyChainUpdates.generateBatch(this.chain, opts) + } + + /** + * Applies EVM-equivalent remote-chain configuration changes with the pool owner wallet. + * + * @remarks Removals run before additions. Each added chain is initialized, configured, and + * rate-limited as one transaction group. Groups are submitted sequentially and are not atomic; + * if a later transaction fails, earlier groups may already be committed. The result contains every + * transaction hash. To replace a chain, include its selector in both `remoteChainSelectorsToRemove` + * and `chainsToAdd`. `wallet` must be the + * pool owner and is the fee payer and default authority. + * + * @see {@link generateUnsignedApplyChainUpdates} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter or chain update is invalid, or the + * authority differs from the executing wallet. + * @throws {@link CCTTxFailedError} If a chain config already exists or is missing, the wallet is + * not the pool owner, or simulation/submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.applyChainUpdates({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelectorsToRemove: [], + * chainsToAdd: [{ + * remoteChainSelector: selector, + * remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + * remotePoolAddresses: ['0x1234567890abcdef1234567890abcdef12345678'], + * remoteTokenDecimals: 18, + * inboundRateLimiterConfig: { enabled: false }, + * outboundRateLimiterConfig: { enabled: false }, + * }], + * wallet, + * }) + * ``` + */ + applyChainUpdates(opts: ExecuteApplyChainUpdatesParams): Promise { + return this.#applyChainUpdates.executeBatch(this.chain, opts) + } + + /** + * Builds an unsigned instruction that appends remote pool addresses to an initialized Solana + * token pool remote-chain config. Pass canonical `poolType` or a compatible + * `poolProgramAddress`; `authority` defaults to `payer`. + * + * @remarks `remotePoolAddresses` must be non-empty and contain no duplicates. Existing addresses + * are retained. On-chain execution rejects addresses already present. To clear all pools, use + * `generateUnsignedEditChainRemoteConfig` with `remotePoolAddresses: []`. The remote-chain config + * must already exist. + * + * @see {@link appendRemotePoolAddresses} + * @see {@link generateUnsignedEditChainRemoteConfig} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter, selector, or remote pool address is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedAppendRemotePoolAddresses({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelector: 5009297550715157269n, + * remotePoolAddresses: ['0x1234567890abcdef1234567890abcdef12345678'], + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedAppendRemotePoolAddresses( + opts: GenerateAppendRemotePoolAddressesParams, + ): Promise { + return this.#appendRemotePoolAddresses.generate(this.chain, opts) + } + + /** + * Appends remote pool addresses to an initialized Solana token pool remote-chain config with the + * pool owner wallet. + * + * @remarks `remotePoolAddresses` must be non-empty and contain no duplicates. Existing addresses + * are retained. The remote-chain config must already exist; addresses already on-chain cause the + * transaction to fail. To clear all pools, use `editChainRemoteConfig` with + * `remotePoolAddresses: []`. + * + * @see {@link generateUnsignedAppendRemotePoolAddresses} + * @see {@link editChainRemoteConfig} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter or remote pool address is invalid, or + * the authority differs from the executing wallet. + * @throws {@link CCTTxFailedError} If the chain config does not exist, the wallet is not the pool + * owner, an address already exists, or simulation/submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.appendRemotePoolAddresses({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelector: 5009297550715157269n, + * remotePoolAddresses: ['0x1234567890abcdef1234567890abcdef12345678'], + * wallet, + * }) + * ``` + */ + appendRemotePoolAddresses( + opts: ExecuteAppendRemotePoolAddressesParams, + ): Promise { + return this.#appendRemotePoolAddresses.execute(this.chain, opts) + } + + /** + * Builds an unsigned instruction that initializes a Solana token pool remote-chain config for a + * previously unconfigured selector. Pass canonical `poolType` or a compatible + * `poolProgramAddress`; `authority` defaults to + * `payer`. + * + * @remarks This creates the chain-config PDA once and fails if it already exists. Configure + * remote pools and rate limits separately before using the lane. + * + * @see {@link initChainRemoteConfig} + * @see {@link generateUnsignedEditChainRemoteConfig} + * @see {@link generateUnsignedDeleteChainRemoteConfig} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter or remote config value is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedInitChainRemoteConfig({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelector: 5009297550715157269n, + * remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + * remoteTokenDecimals: 18, + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedInitChainRemoteConfig( + opts: GenerateInitChainRemoteConfigParams, + ): Promise { + return this.#initChainRemoteConfig.generate(this.chain, opts) + } + + /** + * Initializes a Solana token pool remote-chain config for a previously unconfigured selector + * with the pool owner wallet. + * + * @remarks This creates the chain-config PDA once and fails if it already exists. Configure + * remote pools and rate limits separately before using the lane. + * + * @see {@link generateUnsignedInitChainRemoteConfig} + * @see {@link editChainRemoteConfig} + * @see {@link deleteChainRemoteConfig} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid or the authority differs + * from the executing wallet. + * @throws {@link CCTTxFailedError} If simulation or the pool rejects the transaction. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.initChainRemoteConfig({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelector: 5009297550715157269n, + * remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + * remoteTokenDecimals: 18, + * wallet, + * }) + * ``` + */ + initChainRemoteConfig( + opts: ExecuteInitChainRemoteConfigParams, + ): Promise { + return this.#initChainRemoteConfig.execute(this.chain, opts) + } + + /** + * Builds an unsigned instruction that closes a Solana token pool remote-chain config. Pass + * canonical `poolType` or a compatible `poolProgramAddress`; `authority` defaults to `payer`. + * + * @remarks + * Destructive: this closes the remote-chain config account and returns its rent to `authority`. + * CCIP transfers for `remoteChainSelector` fail until the config is recreated with + * `generateUnsignedInitChainRemoteConfig`. On-chain execution requires `authority` to be the + * token pool owner and the chain config to exist. + * + * @see {@link deleteChainRemoteConfig} + * @see {@link generateUnsignedInitChainRemoteConfig} + * @see {@link generateUnsignedEditChainRemoteConfig} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter or remote chain selector is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedDeleteChainRemoteConfig({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelector: 5009297550715157269n, + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedDeleteChainRemoteConfig( + opts: GenerateDeleteChainRemoteConfigParams, + ): Promise { + return this.#deleteChainRemoteConfig.generate(this.chain, opts) + } + + /** + * Closes an initialized Solana token pool remote-chain config with the pool owner wallet. + * + * @remarks + * Destructive: this closes the remote-chain config account and returns its rent to the wallet. + * CCIP transfers for `remoteChainSelector` fail until the config is recreated with + * `initChainRemoteConfig`. + * + * @see {@link generateUnsignedDeleteChainRemoteConfig} + * @see {@link initChainRemoteConfig} + * @see {@link editChainRemoteConfig} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid or the authority differs + * from the executing wallet. + * @throws {@link CCTTxFailedError} If the chain config does not exist, the wallet is not the pool + * owner, or simulation/submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.deleteChainRemoteConfig({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelector: 5009297550715157269n, + * wallet, + * }) + * ``` + */ + deleteChainRemoteConfig( + opts: ExecuteDeleteChainRemoteConfigParams, + ): Promise { + return this.#deleteChainRemoteConfig.execute(this.chain, opts) + } + + /** + * Builds an unsigned instruction that assigns the rate-limit admin for an initialized Solana + * token pool. Pass canonical `poolType` or a compatible `poolProgramAddress`; `authority` + * defaults to `payer`. + * + * @remarks On-chain execution requires `authority` to be the pool owner. This assignment takes + * effect immediately; unlike ownership transfer, it has no acceptance step. The new rate-limit + * admin may configure chain rate limits but cannot change this role. + * + * @see {@link setRateLimitAdmin} + * @see {@link generateUnsignedSetChainRateLimit} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter or public key is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedSetRateLimitAdmin({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * newRateLimitAdmin, + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedSetRateLimitAdmin( + opts: GenerateSetRateLimitAdminParams, + ): Promise { + return this.#setRateLimitAdmin.generate(this.chain, opts) + } + + /** + * Assigns the rate-limit admin for an initialized Solana token pool with the pool owner wallet. + * + * @remarks This assignment takes effect immediately; unlike ownership transfer, it has no + * acceptance step. The new rate-limit admin may configure chain rate limits but cannot change + * this role. + * + * @see {@link generateUnsignedSetRateLimitAdmin} + * @see {@link setChainRateLimit} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid or the authority differs + * from the executing wallet. + * @throws {@link CCTTxFailedError} If the pool does not exist, the wallet is not the pool owner, + * or simulation/submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.setRateLimitAdmin({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * newRateLimitAdmin, + * wallet, + * }) + * ``` + */ + setRateLimitAdmin(opts: ExecuteSetRateLimitAdminParams): Promise { + return this.#setRateLimitAdmin.execute(this.chain, opts) + } + + /** + * Builds an unsigned instruction that proposes a new owner for an initialized Solana token pool. + * Pass canonical `poolType` or a compatible `poolProgramAddress`; `authority` defaults to `payer`. + * The operation reads pool state and rejects the current owner or default public key. The proposed + * owner must accept ownership separately before the transfer takes effect. + * + * @see {@link transferOwnership} + * @see {@link generateUnsignedAcceptOwnership} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter or public key is invalid. + * @throws {@link CCIPTokenPoolStateNotFoundError} If the token pool account does not exist. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedTransferOwnership({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * newOwner, + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedTransferOwnership( + opts: GenerateTransferOwnershipParams, + ): Promise { + return this.#transferOwnership.generate(this.chain, opts) + } + + /** + * Proposes a new owner for an initialized Solana token pool using the current owner wallet. + * It rejects the current owner or default public key. The proposed owner must accept ownership + * separately before the transfer takes effect. + * + * @see {@link generateUnsignedTransferOwnership} + * @see {@link acceptOwnership} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid or the authority differs + * from the executing wallet. + * @throws {@link CCIPTokenPoolStateNotFoundError} If the token pool account does not exist. + * @throws {@link CCTTxFailedError} If the wallet is not the pool owner or simulation/submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.transferOwnership({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * newOwner, + * wallet, + * }) + * ``` + */ + transferOwnership(opts: ExecuteTransferOwnershipParams): Promise { + return this.#transferOwnership.execute(this.chain, opts) + } + + /** + * Builds an unsigned instruction that accepts pending ownership of an initialized Solana token + * pool. Pass canonical `poolType` or a compatible `poolProgramAddress`; `authority` defaults to + * `payer`. The operation reads pool state and requires it to be the proposed owner. + * + * @see {@link acceptOwnership} + * @see {@link generateUnsignedTransferOwnership} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter or public key is invalid. + * @throws {@link CCIPTokenPoolStateNotFoundError} If the token pool account does not exist. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedAcceptOwnership({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedAcceptOwnership( + opts: GenerateAcceptOwnershipParams, + ): Promise { + return this.#acceptOwnership.generate(this.chain, opts) + } + + /** + * Accepts pending ownership of an initialized Solana token pool using the proposed owner wallet. + * It verifies the wallet is the proposed owner before submitting. + * + * @see {@link generateUnsignedAcceptOwnership} + * @see {@link transferOwnership} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid or the authority differs + * from the executing wallet. + * @throws {@link CCIPTokenPoolStateNotFoundError} If the token pool account does not exist. + * @throws {@link CCTTxFailedError} If the wallet is not the proposed owner or + * simulation/submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.acceptOwnership({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * wallet, + * }) + * ``` + */ + acceptOwnership(opts: ExecuteAcceptOwnershipParams): Promise { + return this.#acceptOwnership.execute(this.chain, opts) + } + + /** + * Builds an unsigned instruction that sets inbound and outbound rate limits for an initialized + * Solana token pool remote-chain config. Pass canonical `poolType` or a compatible + * `poolProgramAddress`; `authority` defaults to `payer`. + * + * @remarks On-chain execution requires `authority` to be the pool owner or rate-limit admin. + * The remote-chain config must already exist. Enabled limits require `rate <= capacity`; + * disabled limits default omitted values to zero and reject nonzero values. + * + * @see {@link setChainRateLimit} + * @see {@link generateUnsignedInitChainRemoteConfig} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter, rate limit, or selector is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedSetChainRateLimit({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelector: 5009297550715157269n, + * inbound: { enabled: true, capacity: 1_000_000n, rate: 1_000n }, + * outbound: { enabled: false }, // Disabled limits default capacity and rate to zero. + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedSetChainRateLimit( + opts: GenerateSetChainRateLimitParams, + ): Promise { + return this.#setChainRateLimit.generate(this.chain, opts) + } + + /** + * Sets inbound and outbound rate limits for an initialized Solana token pool remote-chain config + * with the pool owner or rate-limit admin wallet. + * + * @remarks The remote-chain config must already exist. Enabled limits require `rate <= capacity`; + * disabled limits default omitted values to zero and reject nonzero values. + * + * @see {@link generateUnsignedSetChainRateLimit} + * @see {@link initChainRemoteConfig} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter or rate limit is invalid, or the + * authority differs from the executing wallet. + * @throws {@link CCTTxFailedError} If the chain config does not exist, the wallet is neither the + * pool owner nor rate-limit admin, or simulation/submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.setChainRateLimit({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelector: 5009297550715157269n, + * inbound: { enabled: true, capacity: 1_000_000n, rate: 1_000n }, + * outbound: { enabled: false }, // Disabled limits default capacity and rate to zero. + * wallet, + * }) + * ``` + */ + setChainRateLimit(opts: ExecuteSetChainRateLimitParams): Promise { + return this.#setChainRateLimit.execute(this.chain, opts) + } + + /** + * Builds an unsigned instruction that replaces an initialized Solana token pool remote-chain + * config. Initialize the config first with `generateUnsignedInitChainRemoteConfig`. Each call + * replaces the remote token address, pool addresses, and decimals. Pass canonical `poolType` or + * a compatible `poolProgramAddress`; `authority` defaults to `payer`. + * + * @see {@link editChainRemoteConfig} + * @see {@link generateUnsignedInitChainRemoteConfig} + * @see {@link generateUnsignedDeleteChainRemoteConfig} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter or remote config value is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedEditChainRemoteConfig({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelector: 5009297550715157269n, + * remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + * remotePoolAddresses: ['0x1234567890abcdef1234567890abcdef12345678'], + * remoteTokenDecimals: 18, + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedEditChainRemoteConfig( + opts: GenerateEditChainRemoteConfigParams, + ): Promise { + return this.#editChainRemoteConfig.generate(this.chain, opts) + } + + /** + * Replaces an initialized Solana token pool remote-chain config with the pool owner wallet. + * Initialize the config first with `initChainRemoteConfig`. Each call replaces the remote token + * address, pool addresses, and decimals. + * + * @see {@link generateUnsignedEditChainRemoteConfig} + * @see {@link initChainRemoteConfig} + * @see {@link deleteChainRemoteConfig} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid or the authority differs + * from the executing wallet. + * @throws {@link CCTTxFailedError} If simulation or the pool rejects the transaction. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.editChainRemoteConfig({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelector: 5009297550715157269n, + * remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + * remotePoolAddresses: ['0x1234567890abcdef1234567890abcdef12345678'], + * remoteTokenDecimals: 18, + * wallet, + * }) + * ``` + */ + editChainRemoteConfig( + opts: ExecuteEditChainRemoteConfigParams, + ): Promise { + return this.#editChainRemoteConfig.execute(this.chain, opts) + } + + /** + * Builds unsigned Solana lookup table extend instructions. + * + * Pass `tokenAddress` with a canonical `poolType` or custom `poolProgramAddress` to append the + * standard CCIP pool addresses; pass `additionalAddresses` to append manual addresses. `authority` + * defaults to `payer`. + * + * @throws {@link CCTParamsInvalidError} If an address or lookup table parameter is invalid. + * @throws {@link CCIPTokenMintNotFoundError} If the mint does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedAppendToLookupTable({ + * lookupTableAddress, + * payer: squadsVault, + * authority: squadsVault, + * tokenAddress: mint, + * poolProgramAddress: poolProgram, + * additionalAddresses: [extraAccount], + * }) + * ``` + */ + generateUnsignedAppendToLookupTable( + opts: GenerateAppendToLookupTableParams, + ): Promise { + return this.#appendToLookupTable.generate(this.chain, opts) + } + + /** + * Extends a Solana lookup table. + * + * Pass `tokenAddress` with a canonical `poolType` or custom `poolProgramAddress` to append the + * standard CCIP pool addresses. + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address or lookup table parameter is invalid. + * @throws {@link CCIPTokenMintNotFoundError} If the mint does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * @throws {@link CCTTxFailedError} If transaction simulation or submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.appendToLookupTable({ + * lookupTableAddress, + * wallet, + * tokenAddress: mint, + * poolProgramAddress: poolProgram, + * additionalAddresses: [extraAccount], + * }) + * ``` + */ + appendToLookupTable( + opts: ExecuteAppendToLookupTableParams, + ): Promise { + return this.#appendToLookupTable.execute(this.chain, opts) + } + + /** + * Builds an unsigned Solana instruction that accepts a pending token administrator role. + * + * The supplied authority must be the pending token administrator. + * + * @remarks + * Call this after {@link generateUnsignedRegisterAdmin} or {@link generateUnsignedTransferAdmin} + * and before {@link generateUnsignedSetPool}. `authority` defaults to `payer`; Squads/vault + * flows should use this method with their fee payer and signing authority explicitly. + * + * @see {@link generateUnsignedRegisterAdmin} + * @see {@link generateUnsignedTransferAdmin} + * @see {@link generateUnsignedSetPool} + * + * @throws {@link CCTParamsInvalidError} If an address is invalid or the authority is not the + * pending token administrator. + * @throws {@link CCIPContractNotRouterError} If `address` does not resolve to a Router. + * @throws {@link CCIPTokenNotConfiguredError} If the token is not registered. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedAcceptAdmin({ + * tokenAddress: mint, + * address: router, + * payer: pendingAdmin, + * }) + * ``` + */ + generateUnsignedAcceptAdmin(opts: GenerateAcceptAdminParams): Promise { + return this.#acceptAdmin.generate(this.chain, opts) + } + + /** + * Accepts a pending token administrator role using the pending administrator wallet. + * + * @remarks + * Call this after {@link registerAdmin} or {@link transferAdmin} and before {@link setPool}. + * `authority` defaults to `wallet`; Squads/vault flows should use + * {@link generateUnsignedAcceptAdmin} instead. + * + * @see {@link registerAdmin} + * @see {@link transferAdmin} + * @see {@link setPool} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address is invalid or `authority` does not match + * the executing wallet/pending token administrator. + * @throws {@link CCIPContractNotRouterError} If `address` does not resolve to a Router. + * @throws {@link CCIPTokenNotConfiguredError} If the token is not registered. + * @throws {@link CCTTxFailedError} If simulation or the Router rejects the transaction. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.acceptAdmin({ tokenAddress: mint, address: router, wallet: pendingAdminWallet }) + * ``` + */ + acceptAdmin(opts: ExecuteAcceptAdminParams): Promise { + return this.#acceptAdmin.execute(this.chain, opts) + } + + /** + * Builds an unsigned Solana token registration instruction. + * + * This proposes the registry administrator. The proposed admin must accept the role using + * {@link generateUnsignedAcceptAdmin} before calling {@link generateUnsignedSetPool}. The + * administrator defaults to the mint authority and the method to `owner`; + * choose `ccip-admin` when the Router CCIP admin signs. Provide `administrator` + * to nominate a different admin or register a mint with no mint authority. + * + * @see {@link generateUnsignedAcceptAdmin} + * @see {@link generateUnsignedSetPool} + * + * @throws {@link CCTParamsInvalidError} If an address or `registrationMethod` is invalid, the + * authority does not match the selected registration method, `administrator` is required, or a + * registry entry already exists for the token. + * @throws {@link CCIPContractNotRouterError} If `address` does not resolve to a Router. + * @throws {@link CCIPTokenMintNotFoundError} If the mint does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedRegisterAdmin({ + * tokenAddress: mint, + * address: router, + * payer: mintAuthority, + * }) + * ``` + */ + generateUnsignedRegisterAdmin( + opts: GenerateRegisterAdminParams, + ): Promise { + return this.#registerAdmin.generate(this.chain, opts) + } + + /** + * Proposes a token registry administrator using the executing wallet as registration authority + * and fee payer. The proposed admin must {@link acceptAdmin} before calling {@link setPool}. + * + * @see {@link acceptAdmin} + * @see {@link setPool} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address or `registrationMethod` is invalid, the + * authority does not match the selected registration method or executing wallet, + * `administrator` is required, or a registry entry already exists for the token. + * @throws {@link CCIPContractNotRouterError} If `address` does not resolve to a Router. + * @throws {@link CCIPTokenMintNotFoundError} If the mint does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * @throws {@link CCTTxFailedError} If simulation or the Router rejects the transaction. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.registerAdmin({ + * tokenAddress: mint, + * address: router, + * wallet, + * }) + * ``` + */ + registerAdmin(opts: ExecuteRegisterAdminParams): Promise { + return this.#registerAdmin.execute(this.chain, opts) + } + + /** + * Builds an unsigned instruction to remove addresses from a token pool allowlist. The pool must + * be initialized first. Pass canonical `poolType` or a compatible `poolProgramAddress`; + * `authority` defaults to `payer`. Every removed address must already be allowlisted or the + * transaction reverts. + * + * @remarks Removal does not change enforcement; removing the last allowed sender while the + * allowlist is enabled blocks all senders — use `configureAllowlist` to toggle. + * + * @see {@link generateUnsignedConfigureAllowlist} + * @see {@link removeFromAllowlist} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedRemoveFromAllowlist({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remove: [sender], + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedRemoveFromAllowlist( + opts: GenerateRemoveFromAllowlistParams, + ): Promise { + return this.#removeFromAllowlist.generate(this.chain, opts) + } + + /** + * Removes addresses from an initialized Solana token pool allowlist using the pool owner wallet. + * Every removed address must already be allowlisted or the transaction reverts. + * + * @remarks Removal does not change enforcement; removing the last allowed sender while the + * allowlist is enabled blocks all senders — use `configureAllowlist` to toggle. + * + * @see {@link configureAllowlist} + * @see {@link generateUnsignedRemoveFromAllowlist} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid or the authority differs + * from the executing wallet. + * @throws {@link CCTTxFailedError} If transaction simulation or submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.removeFromAllowlist({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remove: [sender], + * wallet, + * }) + * ``` + */ + removeFromAllowlist( + opts: ExecuteRemoveFromAllowlistParams, + ): Promise { + return this.#removeFromAllowlist.execute(this.chain, opts) + } + + /** + * Builds unsigned Solana `setPool` instructions. + * + * The token must first be registered and its proposed administrator accepted. The `payer` pays + * transaction fees; `authority` defaults to `payer`, while Squads/multisig flows should pass + * the token admin/vault authority explicitly. For a newly deployed canonical pool, create the + * pool signer's ATA before calling this operation. + * + * @see {@link generateUnsignedRegisterAdmin} + * @see {@link generateUnsignedAcceptAdmin} + * @see {@link generateUnsignedDeployTokenPool} + * @see {@link generateUnsignedCreateTokenAccount} + * + * @throws {@link CCTParamsInvalidError} If an address or `writableIndexes` is invalid. + * @throws {@link CCIPContractNotRouterError} If `address` does not resolve to a Router. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedSetPool({ + * tokenAddress: mint, + * address: router, + * poolLookupTableAddress: lookupTable, + * payer: squadsVault, + * authority: tokenAdmin, + * }) + * ``` + */ + generateUnsignedSetPool(opts: GenerateSetPoolParams): Promise { + return this.#setPool.generate(this.chain, opts) + } + + /** + * Registers a token pool. The token must first be registered and its proposed administrator + * accepted; the wallet must be the token admin authority. For a newly deployed canonical pool, + * create the pool signer's ATA before calling this operation. + * + * @see {@link registerAdmin} + * @see {@link acceptAdmin} + * @see {@link deployTokenPool} + * @see {@link createTokenAccount} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address or `writableIndexes` is invalid. + * @throws {@link CCIPContractNotRouterError} If `address` does not resolve to a Router. + * @throws {@link CCTTxFailedError} If simulation or the Router rejects the transaction. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.setPool({ + * tokenAddress: mint, + * address: router, + * poolLookupTableAddress: lookupTable, + * wallet, + * }) + * ``` + */ + setPool(opts: ExecuteSetPoolParams): Promise { + return this.#setPool.execute(this.chain, opts) + } + + /** + * Builds an unsigned Solana instruction that transfers a token administrator role. + * + * @remarks + * This transfers an already accepted administrator role; it does not register a token. The + * proposed administrator must call {@link generateUnsignedAcceptAdmin} before becoming the + * current administrator. + * + * @see {@link generateUnsignedAcceptAdmin} + * + * @throws {@link CCTParamsInvalidError} If an address is invalid or the authority is not the + * current token administrator. + * @throws {@link CCIPContractNotRouterError} If `address` does not resolve to a Router. + * @throws {@link CCIPTokenNotConfiguredError} If the token is not registered. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedTransferAdmin({ + * tokenAddress: mint, + * address: router, + * newAdmin, + * payer: currentAdmin, + * }) + * ``` + */ + generateUnsignedTransferAdmin( + opts: GenerateTransferAdminParams, + ): Promise { + return this.#transferAdmin.generate(this.chain, opts) + } + + /** + * Transfers a token administrator role using the executing wallet as the current administrator. + * + * @remarks + * This transfers an already accepted administrator role; it does not register a token. The + * proposed administrator must call {@link acceptAdmin} before becoming the current administrator. + * + * @see {@link acceptAdmin} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address is invalid or `authority` does not match + * the executing wallet/current token administrator. + * @throws {@link CCIPContractNotRouterError} If `address` does not resolve to a Router. + * @throws {@link CCIPTokenNotConfiguredError} If the token is not registered. + * @throws {@link CCTTxFailedError} If simulation or the Router rejects the transaction. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.transferAdmin({ + * tokenAddress: mint, + * address: router, + * newAdmin, + * wallet: currentAdminWallet, + * }) + * ``` + */ + transferAdmin(opts: ExecuteTransferAdminParams): Promise { + return this.#transferAdmin.execute(this.chain, opts) + } + + /** + * Reads all, or one selected, Solana token pool remote-chain configurations. + * + * @remarks Results are keyed by remote network name. Omit `remoteChainSelector` to scan all + * configured remotes; provide it to query one. Rate-limit amounts use the local mint's smallest + * unit. + * + * @throws {@link CCTParamsInvalidError} If the token or pool program address or remote selector is invalid. + * @throws {@link CCIPTokenPoolStateNotFoundError} If the pool state account does not exist. + * @throws {@link CCIPTokenPoolChainConfigNotFoundError} If the selected remote-chain config does not exist. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const remotes = await cct.getTokenPoolRemotes({ + * tokenAddress: mint, + * poolType: 'burn-mint', + * remoteChainSelector: 5009297550715157269n, + * }) + * console.log(remotes) + * ``` + */ + getTokenPoolRemotes(opts: GetTokenPoolRemotesParams): Promise { + return this.#getTokenPoolRemotes.query(this.chain, opts) + } + + /** + * Reads a Lock/Release token pool's state account, whose config also reports its liquidity + * fields (`rebalancer`, `canAcceptLiquidity`). + * + * @remarks The EVM counterpart, `EVMTokenManager.getTokenPoolState`, returns a different shape: + * its fields are flat where these nest under `state.config`, it spells `config.mint` / + * `config.decimals` / `config.rmnRemote` as `token` / `tokenDecimals` / `rmnProxy`, and its + * `version` is the pool's protocol semver (`'2.0.0'`), not the account-layout number returned + * here. `owner`, `rateLimitAdmin` and `router` are named alike on both. + * + * @throws {@link CCTParamsInvalidError} If the token or pool program address is invalid. + * @throws {@link CCIPTokenPoolStateNotFoundError} If the pool state account does not exist. + * @throws {@link CCTDataDecodeError} If the pool state account cannot be decoded. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const state = await cct.getTokenPoolState({ + * poolType: 'lock-release', + * tokenAddress: mint, + * }) + * // config.owner must sign pool writes; config.rateLimitAdmin may set rate limits + * console.log(state.config.owner, state.config.mint, state.config.decimals) + * // lock-release only: who rebalances the pool, and whether it accepts liquidity + * console.log(state.config.rebalancer, state.config.canAcceptLiquidity) + * ``` + */ + getTokenPoolState( + opts: LockReleasePoolProgramRef & { tokenAddress: string }, + ): Promise + /** + * Reads a Burn/Mint or custom token pool's state account; its config carries no liquidity + * fields. Pass `poolProgramAddress` instead of `poolType` for a custom pool program. + */ + getTokenPoolState( + opts: (BurnMintPoolProgramRef | CustomPoolProgramRef) & { tokenAddress: string }, + ): Promise + /** + * Reads a pool state account whose program is not known statically; narrow the result on the + * presence of the lock-release-only config fields. + */ + getTokenPoolState(opts: GetTokenPoolStateParams): Promise + /** + * Implementation for the overloads above; callers always resolve to one of those. + * */ + getTokenPoolState(opts: GetTokenPoolStateParams): Promise { + return this.#getTokenPoolState.query(this.chain, opts) + } + + /** + * Reads a token's TokenAdminRegistry administrator, pending administrator, and pool lookup table. + * + * @throws {@link CCTParamsInvalidError} If `address` or `tokenAddress` is not a valid Solana public key. + * @throws {@link CCIPContractNotRouterError} If `address` does not resolve to a Router. + * @throws {@link CCIPTokenNotConfiguredError} If the token is not registered. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const config = await cct.getTokenAdminRegistry({ + * address: router, + * tokenAddress: mint, + * }) + * ``` + */ + getTokenAdminRegistry(opts: GetTokenAdminRegistryParams): Promise { + return this.#getTokenAdminRegistry.query(this.chain, opts) + } + + /** + * Lists all SPL token mints configured in a Router's TokenAdminRegistry in a single scan; + * pagination is not supported. + * + * @throws {@link CCTParamsInvalidError} If `address` is not a valid Solana public key. + * @throws {@link CCIPContractNotRouterError} If `address` does not resolve to a Router. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const tokens = await cct.getSupportedTokens({ address: router }) + * ``` + */ + getSupportedTokens(opts: GetSupportedTokensParams): Promise { + return this.#getSupportedTokens.query(this.chain, opts) + } + + /** + * Serializes an unsigned Solana CCT tx for external signing. + * + * @throws {@link CCTParamsInvalidError} If `encoding` is unsupported or the transaction uses + * address lookup tables, which legacy-message serialization cannot represent. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedSetPool({ ...params, payer }) + * const base58 = await cct.serializeUnsignedTx(unsigned, payer) + * const base64 = await cct.serializeUnsignedTx(unsigned, payer, 'base64') + * ``` + */ + serializeUnsignedTx( + unsigned: Pick, + payer: string, + encoding?: SerializedSolanaTxEncoding, + ): Promise { + return serializeUnsignedSolanaTx(this.provider, unsigned, payer, encoding) + } +} + +export * from '../errors.ts' +export { + type TokenPoolType, + TOKEN_POOL_PROGRAMS, + deriveTokenPoolSignerPda, + resolveTokenPoolProgram, +} from './programs/token-pool.ts' +export type { TransactionResult } from '../operation.ts' +export type { SerializedSolanaTxEncoding } from './serialize.ts' +export type * from './token/operations/index.ts' +export type * from './token-pool/operations/index.ts' +export type * from './token-admin-registry/operations/index.ts' diff --git a/ccip-sdk/src/cct/solana/operation.test.ts b/ccip-sdk/src/cct/solana/operation.test.ts new file mode 100644 index 00000000..65d812d6 --- /dev/null +++ b/ccip-sdk/src/cct/solana/operation.test.ts @@ -0,0 +1,126 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { SolanaOperation } from './operation.ts' +import { CCIPWalletInvalidError } from '../../errors/index.ts' +import { ChainFamily } from '../../networks.ts' +import type { SolanaChain } from '../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../solana/types.ts' + +class TestOperation extends SolanaOperation<{ value: string }> { + readonly name = 'testOperation' + captured?: string + validated?: string + + protected override validate(params: { payer: string }): void { + this.validated = params.payer + } + + protected buildUnsigned( + _chain: SolanaChain, + params: { payer: string; value: string }, + ): Promise { + this.captured = params.payer + return Promise.resolve({ family: ChainFamily.Solana, instructions: [] }) + } +} + +class ParsedTestOperation extends SolanaOperation< + { value: string }, + UnsignedSolanaTx, + { payer: string; value: number } +> { + readonly name = 'parsedTestOperation' + readonly lifecycle: string[] = [] + captured?: { payer: string; value: number } + + protected override validate(params: { payer: string; value: string }): void { + this.lifecycle.push(`validate:${params.value}`) + } + + protected override parse(params: { payer: string; value: string }): { + payer: string + value: number + } { + this.lifecycle.push(`parse:${params.value}`) + return { ...params, value: Number(params.value) } + } + + protected buildUnsigned( + _chain: SolanaChain, + params: { payer: string; value: number }, + ): Promise { + this.lifecycle.push(`build:${params.value}`) + this.captured = params + return Promise.resolve({ family: ChainFamily.Solana, instructions: [] }) + } +} + +const chain = { logger: console, connection: {} } as unknown as SolanaChain + +describe('SolanaOperation', () => { + it('validates, parses, then builds without mutating input', async () => { + const op = new ParsedTestOperation() + const params = { payer: PublicKey.default.toBase58(), value: '42' } + + await op.generate(chain, params) + + assert.deepEqual(op.lifecycle, ['validate:42', 'parse:42', 'build:42']) + assert.deepEqual(op.captured, { payer: params.payer, value: 42 }) + assert.equal(params.value, '42') + }) + + it('stops before parsing or building when validation fails', async () => { + class RejectingOperation extends ParsedTestOperation { + protected override validate(params: { payer: string; value: string }): void { + this.lifecycle.push(`validate:${params.value}`) + throw new Error('invalid params') + } + } + + const op = new RejectingOperation() + + await assert.rejects(() => op.generate(chain, { payer: 'payer', value: '42' })) + assert.deepEqual(op.lifecycle, ['validate:42']) + }) + + it('uses wallet public key as payer without mutating caller params', async () => { + const op = new TestOperation() + const wallet = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, + } + const params = { value: 'x', payer: PublicKey.default.toBase58(), wallet } + + await op.execute(chain, params) + + assert.equal(op.validated, wallet.publicKey.toBase58()) + assert.equal(op.captured, wallet.publicKey.toBase58()) + assert.equal(params.payer, PublicKey.default.toBase58()) + }) + + it('does not require payer on signed execution params', async () => { + const op = new TestOperation() + const wallet = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, + } + + await op.execute(chain, { value: 'x', wallet }) + + assert.equal(op.captured, wallet.publicKey.toBase58()) + }) + + it('rejects invalid wallets before validation or building unsigned txs', async () => { + const op = new TestOperation() + + await assert.rejects( + () => op.execute(chain, { value: 'x', wallet: {} }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + assert.equal(op.validated, undefined) + assert.equal(op.captured, undefined) + }) +}) diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts new file mode 100644 index 00000000..2b1e2350 --- /dev/null +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -0,0 +1,85 @@ +/** + * Solana {@link Operation} lifecycle: prepare (validate → parse) → build unsigned tx → submit. + * Default execution uses wallet.publicKey as payer; use generateUnsigned* for a custom payer. + * + * @packageDocumentation + */ + +import { CCIPWalletInvalidError } from '../../errors/index.ts' +import type { SolanaChain } from '../../solana/index.ts' +import { type UnsignedSolanaTx, isWallet } from '../../solana/types.ts' +import { type TransactionResult, Operation } from '../operation.ts' +import { submit } from './submit.ts' + +/** Unsigned Solana operation params include an explicit fee payer. */ +export type SolanaGenerateParams

= P & { payer: string } + +/** Signed Solana operation params derive payer from `wallet.publicKey`. */ +export type SolanaExecuteParams

= P & { + wallet: unknown + computeUnits?: number +} + +/** + * Solana CCT write base. Subclasses supply {@link parse} and {@link buildUnsigned}. + * + * Override {@link parse} for validation, defaults, or conversion; it must be overridden whenever + * `Parsed` differs from `SolanaGenerateParams

`. + */ +export abstract class SolanaOperation< + P extends object, + Tx extends UnsignedSolanaTx = UnsignedSolanaTx, + Parsed = SolanaGenerateParams

, +> extends Operation, Tx, TransactionResult> { + /** + * Optional validation hook required by the shared CCT operation contract. + * + * The default performs no validation. Prefer {@link parse} for Solana operation validation and + * normalization; override this only when parsing is unnecessary. + */ + protected validate(_params: SolanaGenerateParams

): void {} + + /** + * Normalize params without mutating the caller's input. + * + * The default returns params unchanged. Override this method whenever `Parsed` differs from + * `SolanaGenerateParams

`, for example to apply defaults, convert values, or validate fields. + */ + protected parse(params: SolanaGenerateParams

): Parsed { + return params as Parsed + } + + /** Validates and normalizes params for generation or custom execution flows. */ + protected prepare(params: SolanaGenerateParams

): Parsed { + this.validate(params) + return this.parse(params) + } + + /** Build instructions from validated, parsed params. */ + protected abstract buildUnsigned(chain: SolanaChain, params: Parsed): Promise + + /** Run {@link prepare} and {@link buildUnsigned}; no signing. */ + async generate(chain: SolanaChain, params: SolanaGenerateParams

): Promise { + return this.buildUnsigned(chain, this.prepare(params)) + } + + /** Validates the wallet and prepares signed execution parameters with it as payer. */ + protected prepareWalletExecution(params: SolanaExecuteParams

) { + const { wallet, computeUnits, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const payer = wallet.publicKey + return { + wallet, + payer, + computeUnits, + parsed: this.prepare({ ...rest, payer: payer.toBase58() } as SolanaGenerateParams

), + } + } + + /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ + async execute(chain: SolanaChain, params: SolanaExecuteParams

): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/programs/alt.ts b/ccip-sdk/src/cct/solana/programs/alt.ts new file mode 100644 index 00000000..1af41600 --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/alt.ts @@ -0,0 +1,101 @@ +import { Buffer } from 'buffer' + +import { getAssociatedTokenAddressSync } from '@solana/spl-token' +import { + AddressLookupTableProgram, + PublicKey, + SystemProgram, + TransactionInstruction, +} from '@solana/web3.js' + +import { deriveFeeBillingTokenConfigPda } from './fee-quoter.ts' +import { deriveExternalTokenPoolsSignerPda, deriveTokenAdminRegistryPda } from './router.ts' +import { deriveTokenPoolConfigPda, deriveTokenPoolSignerPda } from './token-pool.ts' +import type { SolanaChain } from '../../../solana/index.ts' +import { resolveTokenProgram } from '../../../solana/utils.ts' + +const CREATE_LOOKUP_TABLE_DISCRIMINATOR = 0 +const CREATE_LOOKUP_TABLE_DATA_LENGTH = 13 + +type DeriveCcipLookupTableAddressesParams = { + lookupTableAddress: PublicKey + tokenMint: PublicKey + poolProgram: PublicKey +} + +type BuildCreateLookupTableInstructionParams = { + authority: PublicKey + payer: PublicKey + recentSlot: number | bigint +} + +type BuildCreateLookupTableInstructionResult = { + instruction: TransactionInstruction + lookupTableAddress: PublicKey +} + +/** Builds an ALT create instruction without requiring the authority signature. */ +export function buildCreateLookupTableInstruction({ + authority, + payer, + recentSlot, +}: BuildCreateLookupTableInstructionParams): BuildCreateLookupTableInstructionResult { + const recentSlotBigInt = BigInt(recentSlot) + const recentSlotBuffer = Buffer.alloc(8) + recentSlotBuffer.writeBigUInt64LE(recentSlotBigInt) + + const [lookupTableAddress, bump] = PublicKey.findProgramAddressSync( + [authority.toBuffer(), recentSlotBuffer], + AddressLookupTableProgram.programId, + ) + + const data = Buffer.alloc(CREATE_LOOKUP_TABLE_DATA_LENGTH) + data.writeUInt32LE(CREATE_LOOKUP_TABLE_DISCRIMINATOR, 0) + data.writeBigUInt64LE(recentSlotBigInt, 4) + data.writeUInt8(bump, 12) + + return { + lookupTableAddress, + instruction: new TransactionInstruction({ + programId: AddressLookupTableProgram.programId, + keys: [ + { pubkey: lookupTableAddress, isSigner: false, isWritable: true }, + { pubkey: authority, isSigner: false, isWritable: false }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + data, + }), + } +} + +/** Derives the standard CCIP token pool addresses stored in a pool lookup table. */ +export async function deriveCcipLookupTableAddresses( + chain: SolanaChain, + { lookupTableAddress, tokenMint, poolProgram }: DeriveCcipLookupTableAddressesParams, +): Promise { + const tokenProgram = await resolveTokenProgram(chain.connection, tokenMint) + const poolConfig = deriveTokenPoolConfigPda(poolProgram, tokenMint) + const { router: routerAddress } = await chain.getTokenPoolConfig(poolConfig.toBase58()) + const router = new PublicKey(routerAddress) + const { feeQuoter } = await chain._getRouterConfig(routerAddress) + + const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, tokenMint) + const poolSigner = deriveTokenPoolSignerPda(poolProgram, tokenMint) + const poolTokenAta = getAssociatedTokenAddressSync(tokenMint, poolSigner, true, tokenProgram) + const feeTokenConfig = deriveFeeBillingTokenConfigPda(feeQuoter, tokenMint) + const routerPoolSigner = deriveExternalTokenPoolsSignerPda(router, poolProgram) + + return [ + lookupTableAddress, + tokenAdminRegistry, + poolProgram, + poolConfig, + poolTokenAta, + poolSigner, + tokenProgram, + tokenMint, + feeTokenConfig, + routerPoolSigner, + ] +} diff --git a/ccip-sdk/src/cct/solana/programs/fee-quoter.ts b/ccip-sdk/src/cct/solana/programs/fee-quoter.ts new file mode 100644 index 00000000..71b1dd8a --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/fee-quoter.ts @@ -0,0 +1,11 @@ +import { Buffer } from 'buffer' + +import { PublicKey } from '@solana/web3.js' + +/** Derives the FeeQuoter billing token config PDA for a mint. */ +export function deriveFeeBillingTokenConfigPda(feeQuoter: PublicKey, mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('fee_billing_token_config'), mint.toBuffer()], + feeQuoter, + )[0] +} diff --git a/ccip-sdk/src/cct/solana/programs/router.ts b/ccip-sdk/src/cct/solana/programs/router.ts new file mode 100644 index 00000000..7df7abc3 --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/router.ts @@ -0,0 +1,37 @@ +import { Buffer } from 'buffer' + +import { Program } from '@coral-xyz/anchor' +import { PublicKey } from '@solana/web3.js' + +import { IDL as CCIP_ROUTER_IDL } from '../../../solana/idl/1.6.0/CCIP_ROUTER.ts' +import type { SolanaChain } from '../../../solana/index.ts' +import { simulationProvider } from '../../../solana/utils.ts' + +/** Creates an Anchor Program client for the CCIP Router program. */ +export function createRouterProgram(chain: SolanaChain, router: PublicKey, payer: PublicKey) { + return new Program(CCIP_ROUTER_IDL, router, simulationProvider(chain, payer)) +} + +/** Derives the Router config PDA. */ +export function deriveRouterConfigPda(router: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync([Buffer.from('config')], router)[0] +} + +/** Derives the Router token admin registry PDA for a mint. */ +export function deriveTokenAdminRegistryPda(router: PublicKey, mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('token_admin_registry'), mint.toBuffer()], + router, + )[0] +} + +/** Derives the Router external token pools signer PDA for a pool program. */ +export function deriveExternalTokenPoolsSignerPda( + router: PublicKey, + poolProgram: PublicKey, +): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('external_token_pools_signer'), poolProgram.toBuffer()], + router, + )[0] +} diff --git a/ccip-sdk/src/cct/solana/programs/token-pool.ts b/ccip-sdk/src/cct/solana/programs/token-pool.ts new file mode 100644 index 00000000..9cad1176 --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/token-pool.ts @@ -0,0 +1,147 @@ +import { Buffer } from 'buffer' + +import { Program } from '@coral-xyz/anchor' +import { PublicKey } from '@solana/web3.js' + +import { CCIPError } from '../../../errors/index.ts' +import { + type TokenPoolConfig, + TOKEN_POOL_IDL, + tokenPoolCoder, +} from '../../../solana/idl/token-pool-coder.ts' +export type { TokenPoolConfig } from '../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../solana/index.ts' +import { simulationProvider } from '../../../solana/utils.ts' +import { CCTDataDecodeError } from '../../errors.ts' + +/** Canonical Solana token pool program addresses. */ +export const TOKEN_POOL_PROGRAMS = { + 'burn-mint': '41FGToCmdaWa1dgZLKFAjvmx6e6AjVTX7SVRibvsMGVB', + 'lock-release': '8eqh8wppT9c5rw4ERqNCffvU6cNFJWff9WmkcYtmGiqC', +} as const + +/** Canonical Solana token pool program type. */ +export type TokenPoolType = keyof typeof TOKEN_POOL_PROGRAMS + +/** Identifies a canonical burn-mint token pool program. */ +export type BurnMintPoolProgramRef = { + poolType: 'burn-mint' + poolProgramAddress?: never +} + +/** Identifies a canonical lock-release token pool program. */ +export type LockReleasePoolProgramRef = { + poolType: 'lock-release' + poolProgramAddress?: never +} + +/** Identifies a custom token pool program. */ +export type CustomPoolProgramRef = { + poolProgramAddress: string + poolType?: never +} + +/** Identifies a canonical token pool or a custom pool program. */ +export type PoolProgramRef = + BurnMintPoolProgramRef | LockReleasePoolProgramRef | CustomPoolProgramRef + +type TokenPoolStateDecodeContext = { + tokenPool: string + mint: string + poolProgram: string + accountOwner: string +} + +/** + * Resolves a canonical token pool program type to its address. + * + * @example + * ```ts + * const poolProgram = resolveTokenPoolProgram('burn-mint') + * ``` + */ +export function resolveTokenPoolProgram(poolType: TokenPoolType): PublicKey { + return new PublicKey(TOKEN_POOL_PROGRAMS[poolType]) +} + +/** Creates an Anchor Program client for a token pool program. */ +export function createTokenPoolProgram( + chain: SolanaChain, + poolProgram: PublicKey, + payer: PublicKey, +) { + return new Program(TOKEN_POOL_IDL, poolProgram, simulationProvider(chain, payer)) +} + +/** Decodes a canonical token pool state account. */ +export function decodeTokenPoolState( + data: Buffer, + context: TokenPoolStateDecodeContext, +): { version: number; config: TokenPoolConfig } { + try { + return tokenPoolCoder.accounts.decode<{ version: number; config: TokenPoolConfig }>( + 'state', + data, + ) + } catch (cause) { + throw new CCTDataDecodeError(context.tokenPool, { + cause: cause instanceof Error ? cause : CCIPError.from(cause), + context: { + mint: context.mint, + poolProgram: context.poolProgram, + accountOwner: context.accountOwner, + }, + }) + } +} + +/** Derives the token pool global config PDA. */ +export function deriveTokenPoolGlobalConfigPda(poolProgram: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync([Buffer.from('config')], poolProgram)[0] +} + +/** Derives a token pool state/config PDA for a mint. */ +export function deriveTokenPoolConfigPda(poolProgram: PublicKey, mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('ccip_tokenpool_config'), mint.toBuffer()], + poolProgram, + )[0] +} + +/** + * Derives a token pool signer PDA for a mint. + * + * @example + * ```ts + * const poolProgram = resolveTokenPoolProgram('burn-mint') + * const poolSigner = deriveTokenPoolSignerPda(poolProgram, new PublicKey(tokenAddress)) + * ``` + */ +export function deriveTokenPoolSignerPda(poolProgram: PublicKey, mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('ccip_tokenpool_signer'), mint.toBuffer()], + poolProgram, + )[0] +} + +/** Derives a token pool chain configuration PDA. */ +export function deriveTokenPoolChainConfigPda( + poolProgram: PublicKey, + remoteChainSelector: bigint, + mint: PublicKey, +): PublicKey { + const selector = Buffer.alloc(8) + selector.writeBigUInt64LE(remoteChainSelector) + return PublicKey.findProgramAddressSync( + [Buffer.from('ccip_tokenpool_chainconfig'), selector, mint.toBuffer()], + poolProgram, + )[0] +} + +/** Derives the token pool program data PDA. */ +export function deriveTokenPoolProgramDataPda(poolProgram: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [poolProgram.toBuffer()], + new PublicKey('BPFLoaderUpgradeab1e11111111111111111111111'), + )[0] +} diff --git a/ccip-sdk/src/cct/solana/query.ts b/ccip-sdk/src/cct/solana/query.ts new file mode 100644 index 00000000..4ee77570 --- /dev/null +++ b/ccip-sdk/src/cct/solana/query.ts @@ -0,0 +1,17 @@ +/** + * Solana CCT reads: {@link Query} bound to a {@link SolanaChain}. The read-only counterpart of + * {@link SolanaOperation} — no wallet, no instructions, no submit. + * + * @packageDocumentation + */ + +import type { SolanaChain } from '../../solana/index.ts' +import { Query } from '../query.ts' + +/** Shared base for read-only Solana CCT queries; see {@link Query}. */ +export abstract class SolanaQuery

extends Query< + SolanaChain, + P, + R, + Parsed +> {} diff --git a/ccip-sdk/src/cct/solana/serialize.test.ts b/ccip-sdk/src/cct/solana/serialize.test.ts new file mode 100644 index 00000000..75835b10 --- /dev/null +++ b/ccip-sdk/src/cct/solana/serialize.test.ts @@ -0,0 +1,56 @@ +import { Buffer } from 'buffer' +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Message, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js' +import bs58 from 'bs58' + +import { serializeUnsignedSolanaTx } from './serialize.ts' +import { CCTParamsInvalidError } from '../errors.ts' + +const KEY = PublicKey.default +const connection = { + getLatestBlockhash: async () => ({ blockhash: KEY.toBase58(), lastValidBlockHeight: 0 }), +} +const unsigned = { + instructions: [ + new TransactionInstruction({ + programId: SystemProgram.programId, + keys: [], + data: Buffer.alloc(0), + }), + ], +} + +describe('Serialize (cct/solana)', () => { + it('serializes unsigned Solana txs as legacy messages in supported encodings', async () => { + const base58 = await serializeUnsignedSolanaTx(connection, unsigned, KEY) + const base64 = await serializeUnsignedSolanaTx(connection, unsigned, KEY, 'base64') + const hex = await serializeUnsignedSolanaTx(connection, unsigned, KEY, 'hex') + + assert.ok(Message.from(bs58.decode(base58))) + assert.ok(Message.from(Buffer.from(base64, 'base64'))) + assert.ok(Message.from(Buffer.from(hex, 'hex'))) + }) + + it('rejects lookup tables for legacy message serialization', async () => { + await assert.rejects( + () => + serializeUnsignedSolanaTx(connection, { ...unsigned, lookupTables: [{} as never] }, KEY), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'serializeUnsignedTx' && + err.context.param === 'lookupTables', + ) + }) + + it('rejects unsupported transaction encodings', async () => { + await assert.rejects( + () => serializeUnsignedSolanaTx(connection, unsigned, KEY, 'base32'), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'serializeUnsignedTx' && + err.context.param === 'encoding', + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/serialize.ts b/ccip-sdk/src/cct/solana/serialize.ts new file mode 100644 index 00000000..3bb81125 --- /dev/null +++ b/ccip-sdk/src/cct/solana/serialize.ts @@ -0,0 +1,48 @@ +import { Buffer } from 'buffer' + +import { PublicKey, TransactionMessage } from '@solana/web3.js' +import bs58 from 'bs58' + +import type { UnsignedSolanaTx } from '../../solana/types.ts' +import { CCTParamsInvalidError } from '../errors.ts' + +/** Supported serialized transaction encodings. */ +export type SerializedSolanaTxEncoding = 'base58' | 'base64' | 'hex' + +/** Serializes an unsigned Solana tx into one legacy message for external signing. */ +export async function serializeUnsignedSolanaTx( + connection: { getLatestBlockhash: () => Promise<{ blockhash: string }> }, + unsigned: Pick, + payer: PublicKey | string, + encoding = 'base58', +): Promise { + if (unsigned.lookupTables?.length) { + throw new CCTParamsInvalidError( + 'serializeUnsignedTx', + 'lookupTables', + 'legacy-message serialization does not support address lookup tables', + ) + } + + const payerKey = typeof payer === 'string' ? new PublicKey(payer) : payer + const { blockhash } = await connection.getLatestBlockhash() + const serialized = Buffer.from( + new TransactionMessage({ + payerKey, + recentBlockhash: blockhash, + instructions: unsigned.instructions, + }) + .compileToLegacyMessage() + .serialize(), + ) + + if (encoding === 'base58') return bs58.encode(serialized) + if (encoding === 'base64') return serialized.toString('base64') + if (encoding === 'hex') return serialized.toString('hex') + + throw new CCTParamsInvalidError( + 'serializeUnsignedTx', + 'encoding', + `unsupported Solana transaction encoding: ${String(encoding)}`, + ) +} diff --git a/ccip-sdk/src/cct/solana/submit.test.ts b/ccip-sdk/src/cct/solana/submit.test.ts new file mode 100644 index 00000000..d3c4e6ec --- /dev/null +++ b/ccip-sdk/src/cct/solana/submit.test.ts @@ -0,0 +1,74 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { SendTransactionError, TransactionExpiredTimeoutError } from '@solana/web3.js' + +import { CCTTxFailedError, CCTTxNotConfirmedError } from '../errors.ts' +import { createCCTSubmitError } from './submit.ts' + +const OP = 'setPool' + +describe('Submit error mapping (cct/solana)', () => { + it('maps post-broadcast confirmation errors with a signature to not-confirmed', () => { + const cause = Object.assign(new Error('transaction was not confirmed'), { signature: 'abc' }) + const err = createCCTSubmitError(OP, cause) + + assert.ok(err instanceof CCTTxNotConfirmedError) + assert.equal(err.isTransient, true) + assert.equal(err.context.txHash, 'abc') + }) + + it('maps web3.js transaction expiry errors to not-confirmed', () => { + const err = createCCTSubmitError(OP, new TransactionExpiredTimeoutError('def', 30)) + + assert.ok(err instanceof CCTTxNotConfirmedError) + assert.equal(err.context.txHash, 'def') + }) + + it('maps SendTransactionError with a signature to not-confirmed', () => { + const cause = new SendTransactionError({ + action: 'send', + signature: 'ghi', + transactionMessage: 'block height exceeded', + }) + const err = createCCTSubmitError(OP, cause) + + assert.ok(err instanceof CCTTxNotConfirmedError) + assert.equal(err.context.txHash, 'ghi') + }) + + it('maps signed on-chain failures to permanent tx failed', () => { + const cause = Object.assign(new Error('custom program error: 0x1'), { signature: 'jkl' }) + const err = createCCTSubmitError(OP, cause) + + assert.ok(err instanceof CCTTxFailedError) + assert.equal(err.isTransient, false) + assert.equal(err.context.txHash, undefined) + }) + + it('maps SendTransactionError with an empty signature to transient tx failed', () => { + const cause = new SendTransactionError({ + action: 'simulate', + signature: '', + transactionMessage: 'blockhash not found', + }) + const err = createCCTSubmitError(OP, cause) + + assert.ok(err instanceof CCTTxFailedError) + assert.equal(err.isTransient, true) + }) + + it('maps pre-broadcast transient errors to transient tx failed', () => { + const err = createCCTSubmitError(OP, new Error('blockhash not found')) + + assert.ok(err instanceof CCTTxFailedError) + assert.equal(err.isTransient, true) + }) + + it('maps program errors to permanent tx failed', () => { + const err = createCCTSubmitError(OP, new Error('custom program error: 0x1')) + + assert.ok(err instanceof CCTTxFailedError) + assert.equal(err.isTransient, false) + }) +}) diff --git a/ccip-sdk/src/cct/solana/submit.ts b/ccip-sdk/src/cct/solana/submit.ts new file mode 100644 index 00000000..d35e3a84 --- /dev/null +++ b/ccip-sdk/src/cct/solana/submit.ts @@ -0,0 +1,80 @@ +/** + * Shared sign-and-submit pipeline for Solana CCT operations. Maps simulation/program + * failures to permanent {@link CCTTxFailedError}, pre-broadcast infra failures to + * transient {@link CCTTxFailedError}, and post-broadcast confirmation failures to + * {@link CCTTxNotConfirmedError}. + * + * @packageDocumentation + */ + +import { + TransactionExpiredBlockheightExceededError, + TransactionExpiredNonceInvalidError, + TransactionExpiredTimeoutError, +} from '@solana/web3.js' + +import { CCIPWalletInvalidError, shouldRetry } from '../../errors/index.ts' +import type { SolanaChain } from '../../solana/index.ts' +import { type UnsignedSolanaTx, isWallet } from '../../solana/types.ts' +import { simulateAndSendTxs } from '../../solana/utils.ts' +import { CCTTxFailedError, CCTTxNotConfirmedError } from '../errors.ts' +import type { TransactionResult } from '../operation.ts' + +/** Signs, simulates, sends, and confirms a Solana CCT transaction. */ +export async function submit( + chain: SolanaChain, + wallet: unknown, + unsigned: UnsignedSolanaTx, + operation: string, + computeUnits?: number, +): Promise { + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + try { + return { hash: await simulateAndSendTxs(chain, wallet, unsigned, computeUnits) } + } catch (error) { + throw createCCTSubmitError(operation, error) + } +} + +/** Maps Solana submit errors to permanent failed vs transient failed/not-confirmed CCT errors. */ +export function createCCTSubmitError( + operation: string, + error: unknown, +): CCTTxFailedError | CCTTxNotConfirmedError { + const signature = getSignature(error) + if (signature && isNotConfirmedError(error)) { + return new CCTTxNotConfirmedError(operation, signature, { + cause: error instanceof Error ? error : undefined, + }) + } + + return new CCTTxFailedError(operation, getReason(error), { + cause: error instanceof Error ? error : undefined, + isTransient: isTransientSubmitError(error), + }) +} + +function isTransientSubmitError(error: unknown): boolean { + return /blockhash|expired/i.test(getReason(error)) || shouldRetry(error) +} + +function isNotConfirmedError(error: unknown): boolean { + return ( + error instanceof TransactionExpiredBlockheightExceededError || + error instanceof TransactionExpiredNonceInvalidError || + error instanceof TransactionExpiredTimeoutError || + /not confirmed|unknown if it succeeded|block height exceeded/i.test(getReason(error)) + ) +} + +function getReason(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +function getSignature(error: unknown): string | undefined { + if (!error || typeof error !== 'object' || !('signature' in error)) return undefined + return typeof error.signature === 'string' && error.signature.length > 0 + ? error.signature + : undefined +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin.test.ts new file mode 100644 index 00000000..d8266c22 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin.test.ts @@ -0,0 +1,152 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import type { GenerateAcceptAdminParams } from './accept-admin.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveRouterConfigPda, deriveTokenAdminRegistryPda } from '../../programs/router.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const ADDRESS = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const PENDING_ADMIN = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stubChain( + pendingAdministrator = PENDING_ADMIN, + onAddress?: (address: string) => void, +): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + getTokenAdminRegistryFor: async (address: string) => { + onAddress?.(address) + return ROUTER + }, + getRegistryTokenConfig: async () => ({ administrator: PAYER, pendingAdministrator }), + } as unknown as SolanaChain +} + +function generate(opts: Partial = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedAcceptAdmin({ + tokenAddress: TOKEN, + address: ADDRESS, + payer: PAYER, + authority: PENDING_ADMIN, + ...opts, + }) +} + +describe('AcceptAdmin (cct/solana)', () => { + describe('generate', () => { + it('builds an unsigned accept admin instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction.programId.toBase58(), ROUTER) + assert.equal(instruction.data.toString('hex'), '6af010ad89d5a3f6') + assert.deepEqual( + instruction.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveRouterConfigPda(new PublicKey(ROUTER)).toBase58(), + isSigner: false, + isWritable: false, + }, + { + pubkey: deriveTokenAdminRegistryPda( + new PublicKey(ROUTER), + new PublicKey(TOKEN), + ).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: TOKEN, isSigner: false, isWritable: false }, + { pubkey: PENDING_ADMIN, isSigner: true, isWritable: true }, + ], + ) + }) + + it('resolves the router from address', async () => { + let requestedAddress: string | undefined + const cct = SolanaTokenManager.fromChain( + stubChain(PENDING_ADMIN, (address) => (requestedAddress = address)), + ) + + await cct.generateUnsignedAcceptAdmin({ + tokenAddress: TOKEN, + address: ADDRESS, + payer: PAYER, + authority: PENDING_ADMIN, + }) + + assert.equal(requestedAddress, ADDRESS) + }) + }) + + describe('validation', () => { + it('rejects an authority that is not the pending administrator', async () => { + await assert.rejects( + () => generate({ authority: PAYER }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'authority', + ) + }) + + it('rejects when no administrator is pending', async () => { + const noPendingChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + getTokenAdminRegistryFor: async () => ROUTER, + getRegistryTokenConfig: async () => ({ administrator: PAYER }), + } as unknown as SolanaChain + + await assert.rejects( + () => + SolanaTokenManager.fromChain(noPendingChain).generateUnsignedAcceptAdmin({ + tokenAddress: TOKEN, + address: ADDRESS, + payer: PAYER, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'authority' && + typeof err.context.reason === 'string' && + err.context.reason.includes('no administrator is pending'), + ) + }) + }) + + describe('execute', () => { + it('requires the pending admin to be the executing wallet', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).acceptAdmin({ + tokenAddress: TOKEN, + address: ADDRESS, + authority: PENDING_ADMIN, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'authority' && + typeof err.context.reason === 'string' && + err.context.reason.includes('requires authority to be the executing wallet'), + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin.ts new file mode 100644 index 00000000..bf2828f7 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin.ts @@ -0,0 +1,138 @@ +import { PublicKey } from '@solana/web3.js' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { type UnsignedSolanaTx, isWallet } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { submit } from '../../submit.ts' +import { parsePublicKey, validateAuthorityMatchesWallet } from '../../validate.ts' + +/** Parameters shared by Solana TokenAdminRegistry `acceptAdmin` generation and execution. */ +type AcceptAdminParams = { + tokenAddress: string + /** + * CCIP contract to resolve the TokenAdminRegistry/Router from — a Router or OffRamp + * address works. + */ + address: string + /** Pending token admin. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedAcceptAdminParams = { + tokenAddress: PublicKey + address: PublicKey + payer: PublicKey + authority: PublicKey +} + +/** Parameters for unsigned Solana TokenAdminRegistry `acceptAdmin` generation. */ +export type GenerateAcceptAdminParams = SolanaGenerateParams + +/** Unsigned Solana TokenAdminRegistry `acceptAdmin` result. */ +export type GenerateAcceptAdminResult = UnsignedSolanaTx + +/** Parameters for executing Solana TokenAdminRegistry `acceptAdmin`. */ +export type ExecuteAcceptAdminParams = SolanaExecuteParams + +/** Result of executing Solana TokenAdminRegistry `acceptAdmin`. */ +export type ExecuteAcceptAdminResult = TransactionResult + +/** Accepts a pending TokenAdminRegistry administrator role. */ +export class AcceptAdmin extends SolanaOperation< + AcceptAdminParams, + UnsignedSolanaTx, + ParsedAcceptAdminParams +> { + readonly name = 'acceptAdmin' + + /** Parses public keys and defaults authority to payer without mutating caller params. */ + protected override parse(params: GenerateAcceptAdminParams): ParsedAcceptAdminParams { + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + address: parsePublicKey(this.name, 'address', params.address), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + } + } + + /** Builds the unsigned instruction after confirming the caller is the pending admin. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedAcceptAdminParams, + ): Promise { + const { tokenAddress: tokenMint, payer, authority } = opts + const router = new PublicKey(await chain.getTokenAdminRegistryFor(opts.address.toBase58())) + const tokenConfig = await chain.getRegistryTokenConfig(router.toBase58(), tokenMint.toBase58()) + + if (!tokenConfig.pendingAdministrator) { + throw new CCTParamsInvalidError( + this.name, + 'authority', + `no administrator is pending for this token (current administrator: ${tokenConfig.administrator}) — nothing to accept`, + ) + } + if (!new PublicKey(tokenConfig.pendingAdministrator).equals(authority)) { + throw new CCTParamsInvalidError( + this.name, + 'authority', + 'must be the pending token administrator', + ) + } + + const instruction = await createRouterProgram(chain, router, payer) + .methods.acceptAdminRoleTokenAdminRegistry() + .accounts({ + config: deriveRouterConfigPda(router), + tokenAdminRegistry: deriveTokenAdminRegistryPda(router, tokenMint), + mint: tokenMint, + authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: router = ${router.toBase58()}, token = ${tokenMint.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the pending admin wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteAcceptAdminParams, + ): Promise { + const { wallet, computeUnits, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const payer = wallet.publicKey.toBase58() + const generateParams: GenerateAcceptAdminParams = { ...rest, payer } + const parsed = this.prepare(generateParams) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'acceptAdmin requires authority to be the executing wallet. Use generateUnsignedAcceptAdmin for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.test.ts new file mode 100644 index 00000000..39d83a7e --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.test.ts @@ -0,0 +1,273 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { AddressLookupTableProgram, Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveCcipLookupTableAddresses } from '../../programs/alt.ts' +import { TOKEN_POOL_PROGRAMS } from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const POOL_PROGRAM = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const FEE_QUOTER = Keypair.generate().publicKey +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const LOOKUP_TABLE = Keypair.generate().publicKey.toBase58() +const ALT_EXTEND_ADDRESSES_OFFSET = 12 // 4-byte discriminator + 8-byte address vector length +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +type StubChainOptions = { + addresses?: PublicKey[] + authority?: string + onGetLookupTable?: () => void +} + +function stubChain({ + addresses = [], + authority = AUTHORITY, + onGetLookupTable, +}: StubChainOptions = {}): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }), + getAddressLookupTable: async () => { + onGetLookupTable?.() + return { + value: { + state: { + authority: new PublicKey(authority), + addresses, + }, + }, + } + }, + }, + getTokenPoolConfig: async () => ({ + token: TOKEN, + router: ROUTER, + tokenPoolProgram: POOL_PROGRAM, + }), + _getRouterConfig: async () => ({ feeQuoter: FEE_QUOTER }), + } as unknown as SolanaChain +} + +function generate(opts = {}, chain = stubChain()) { + return SolanaTokenManager.fromChain(chain).generateUnsignedAppendToLookupTable({ + lookupTableAddress: LOOKUP_TABLE, + payer: PAYER, + authority: AUTHORITY, + additionalAddresses: [Keypair.generate().publicKey.toBase58()], + ...opts, + }) +} + +describe('AppendToLookupTable (cct/solana)', () => { + describe('generate', () => { + it('builds extend ALT instructions', async () => { + const unsigned = await generate() + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + assert.equal( + unsigned.instructions[0]!.programId.toBase58(), + AddressLookupTableProgram.programId.toBase58(), + ) + }) + + it('chunks additional addresses into multiple extend instructions', async () => { + const additionalAddresses = Array.from({ length: 31 }, () => + Keypair.generate().publicKey.toBase58(), + ) + const unsigned = await generate({ additionalAddresses }) + + assert.equal(unsigned.instructions.length, 2) + }) + + it('appends derived CCIP addresses before manual addresses', async () => { + const chain = stubChain() + const manualAddress = Keypair.generate().publicKey + const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { + lookupTableAddress: new PublicKey(LOOKUP_TABLE), + tokenMint: new PublicKey(TOKEN), + poolProgram: new PublicKey(POOL_PROGRAM), + }) + const unsigned = await generate( + { + tokenAddress: TOKEN, + poolProgramAddress: POOL_PROGRAM, + additionalAddresses: [manualAddress.toBase58()], + }, + chain, + ) + const appendedAddresses = Array.from( + { length: ccipAddresses.length + 1 }, + (_, i) => + new PublicKey( + unsigned.instructions[0]!.data.subarray( + ALT_EXTEND_ADDRESSES_OFFSET + i * 32, + ALT_EXTEND_ADDRESSES_OFFSET + (i + 1) * 32, + ), + ), + ) + + assert.deepEqual( + appendedAddresses.map((address) => address.toBase58()), + [...ccipAddresses, manualAddress].map((address) => address.toBase58()), + ) + }) + + it('accepts a canonical pool type', async () => { + const unsigned = await generate({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + }) + + assert.equal(unsigned.instructions.length, 1) + assert.ok( + unsigned.instructions[0]!.data.includes( + new PublicKey(TOKEN_POOL_PROGRAMS['burn-mint']).toBuffer(), + ), + ) + }) + + it('ignores an undefined unused pool reference', async () => { + const unsigned = await generate({ + tokenAddress: TOKEN, + poolProgramAddress: POOL_PROGRAM, + poolType: undefined, + }) + + assert.equal(unsigned.instructions.length, 1) + }) + + it('rejects auto-derived CCIP addresses when the canonical block already exists', async () => { + const chain = stubChain() + const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { + lookupTableAddress: new PublicKey(LOOKUP_TABLE), + tokenMint: new PublicKey(TOKEN), + poolProgram: new PublicKey(POOL_PROGRAM), + }) + + await assert.rejects( + () => + generate( + { tokenAddress: TOKEN, poolProgramAddress: POOL_PROGRAM }, + stubChain({ addresses: ccipAddresses }), + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'lookupTableAddress', + ) + }) + + it('rejects authority mismatch', async () => { + await assert.rejects( + () => generate({}, stubChain({ authority: Keypair.generate().publicKey.toBase58() })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'authority', + ) + }) + + it('rejects ALTs over 256 addresses', async () => { + const currentAddresses = Array.from({ length: 256 }, () => Keypair.generate().publicKey) + + await assert.rejects( + () => generate({}, stubChain({ addresses: currentAddresses })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'additionalAddresses', + ) + }) + }) + + describe('validation', () => { + it('rejects an ambiguous pool reference before the ALT RPC', async () => { + let getLookupTableCalls = 0 + + await assert.rejects( + SolanaTokenManager.fromChain( + stubChain({ onGetLookupTable: () => getLookupTableCalls++ }), + ).generateUnsignedAppendToLookupTable({ + lookupTableAddress: LOOKUP_TABLE, + payer: PAYER, + tokenAddress: TOKEN, + poolType: 'burn-mint', + poolProgramAddress: POOL_PROGRAM, + } as never), + CCTParamsInvalidError, + ) + + assert.equal(getLookupTableCalls, 0) + }) + + it('rejects an invalid pool program address', async () => { + let getLookupTableCalls = 0 + + await assert.rejects( + SolanaTokenManager.fromChain( + stubChain({ onGetLookupTable: () => getLookupTableCalls++ }), + ).generateUnsignedAppendToLookupTable({ + lookupTableAddress: LOOKUP_TABLE, + payer: PAYER, + tokenAddress: TOKEN, + poolProgramAddress: 'invalid', + }), + CCTParamsInvalidError, + ) + + assert.equal(getLookupTableCalls, 0) + }) + + it('requires at least one address source', async () => { + await assert.rejects( + () => generate({ additionalAddresses: [] }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'additionalAddresses', + ) + }) + + it('requires token and pool program together', async () => { + await assert.rejects( + () => generate({ tokenAddress: TOKEN }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'tokenAddress', + ) + }) + }) + + describe('execute', () => { + it('rejects signed append when authority is not the wallet', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).appendToLookupTable({ + lookupTableAddress: LOOKUP_TABLE, + wallet: WALLET, + authority: AUTHORITY, + additionalAddresses: [Keypair.generate().publicKey.toBase58()], + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.ts new file mode 100644 index 00000000..9766a389 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.ts @@ -0,0 +1,234 @@ +import { + type PublicKey, + type TransactionInstruction, + AddressLookupTableProgram, +} from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { deriveCcipLookupTableAddresses } from '../../programs/alt.ts' +import type { PoolProgramRef } from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, +} from '../../validate.ts' + +const MAX_ALT_ADDRESSES = 256 +const EXTEND_CHUNK_SIZE = 30 + +type AppendAdditionalAddressesParams = { + additionalAddresses: string[] + tokenAddress?: never + poolType?: never + poolProgramAddress?: never +} + +type AppendCanonicalAddressesParams = { + tokenAddress: string + additionalAddresses?: string[] +} & PoolProgramRef + +/** + * Parameters shared by Solana TokenAdminRegistry `appendToLookupTable` generation and execution. + * + * Provide `tokenAddress` with exactly one of `poolType` or `poolProgramAddress` to append the + * canonical CCIP addresses. Additional addresses may also be included. + * + * Otherwise, provide `additionalAddresses` only. + */ +type AppendToLookupTableParams = { + lookupTableAddress: string + /** ALT authority. Defaults to payer for unsigned generation and wallet public key for execute. */ + authority?: string +} & (AppendAdditionalAddressesParams | AppendCanonicalAddressesParams) + +/** Parameters for unsigned Solana lookup table append generation. */ +export type GenerateAppendToLookupTableParams = SolanaGenerateParams + +type ParsedAppendToLookupTableParams = { + payer: PublicKey + authority: PublicKey + lookupTableAddress: PublicKey + additionalAddresses: PublicKey[] + tokenMint?: PublicKey + poolProgram?: PublicKey +} + +/** Unsigned append lookup table result. */ +export type GenerateAppendToLookupTableResult = UnsignedSolanaTx + +/** Parameters for executing Solana TokenAdminRegistry `appendToLookupTable`. */ +export type ExecuteAppendToLookupTableParams = SolanaExecuteParams + +/** Result of executing Solana TokenAdminRegistry `appendToLookupTable`. */ +export type ExecuteAppendToLookupTableResult = TransactionResult + +/** Builds and submits Solana ALT extend instructions for token pool setup. */ +export class AppendToLookupTable extends SolanaOperation< + AppendToLookupTableParams, + GenerateAppendToLookupTableResult, + ParsedAppendToLookupTableParams +> { + readonly name = 'appendToLookupTable' + + /** Parses all public keys before any RPC. */ + protected override parse( + params: GenerateAppendToLookupTableParams, + ): ParsedAppendToLookupTableParams { + const payer = parsePublicKey(this.name, 'payer', params.payer) + const authority = + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority) + const lookupTableAddress = parsePublicKey( + this.name, + 'lookupTableAddress', + params.lookupTableAddress, + ) + + const hasTokenAddress = params.tokenAddress !== undefined + const hasPoolProgramAddress = params.poolProgramAddress !== undefined + const hasPoolProgram = params.poolType !== undefined || hasPoolProgramAddress + if (hasTokenAddress !== hasPoolProgram) { + throw new CCTParamsInvalidError( + this.name, + 'tokenAddress', + 'tokenAddress and exactly one of poolType or poolProgramAddress must be provided together', + ) + } + const tokenMint = + params.tokenAddress === undefined + ? undefined + : parsePublicKey(this.name, 'tokenAddress', params.tokenAddress) + const poolProgram = hasPoolProgram ? resolvePoolProgram(this.name, params) : undefined + const additionalAddresses = (params.additionalAddresses ?? []).map((address, i) => + parsePublicKey(this.name, `additionalAddresses[${i}]`, address), + ) + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (params.tokenAddress === undefined && !params.additionalAddresses?.length) { + throw new CCTParamsInvalidError( + this.name, + 'additionalAddresses', + 'must provide tokenAddress/poolProgramAddress or additionalAddresses', + ) + } + return { + payer, + authority, + lookupTableAddress, + additionalAddresses, + ...(tokenMint !== undefined && { tokenMint }), + ...(poolProgram !== undefined && { poolProgram }), + } + } + + /** Builds unsigned ALT extend instructions. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedAppendToLookupTableParams, + ): Promise { + const { payer, authority, lookupTableAddress, poolProgram } = opts + const lookupTable = await chain.connection.getAddressLookupTable(lookupTableAddress) + + if (!lookupTable.value) { + throw new CCTParamsInvalidError( + this.name, + 'lookupTableAddress', + `lookup table not found: ${lookupTableAddress.toBase58()}`, + ) + } + + if (!lookupTable.value.state.authority?.equals(authority)) { + throw new CCTParamsInvalidError( + this.name, + 'authority', + `authority mismatch; ALT authority is ${lookupTable.value.state.authority?.toBase58() ?? 'none'}`, + ) + } + + const addresses = [...opts.additionalAddresses] + + if (opts.tokenMint && poolProgram) { + const { tokenMint } = opts + const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { + lookupTableAddress, + tokenMint, + poolProgram, + }) + const existingAddresses = new Set( + lookupTable.value.state.addresses.map((address) => address.toBase58()), + ) + + if (ccipAddresses.every((address) => existingAddresses.has(address.toBase58()))) { + throw new CCTParamsInvalidError( + this.name, + 'lookupTableAddress', + 'lookup table already contains the canonical CCIP address block; only append additionalAddresses or use an empty ALT', + ) + } + + addresses.unshift(...ccipAddresses) + } + + const totalAddressesAfterAppend = lookupTable.value.state.addresses.length + addresses.length + if (totalAddressesAfterAppend > MAX_ALT_ADDRESSES) { + throw new CCTParamsInvalidError( + this.name, + 'additionalAddresses', + `ALT cannot exceed ${MAX_ALT_ADDRESSES} addresses; requested ${totalAddressesAfterAppend}`, + ) + } + + const instructions: TransactionInstruction[] = [] + for (let i = 0; i < addresses.length; i += EXTEND_CHUNK_SIZE) { + instructions.push( + AddressLookupTableProgram.extendLookupTable({ + payer, + authority, + lookupTable: lookupTableAddress, + addresses: addresses.slice(i, i + EXTEND_CHUNK_SIZE), + }), + ) + } + + chain.logger.debug( + `${this.name}: lookupTable = ${lookupTableAddress.toBase58()}, appended = ${addresses.length}, total = ${totalAddressesAfterAppend}`, + ) + return { + family: ChainFamily.Solana, + instructions, + mainIndex: 0, + } + } + + /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ + override async execute( + chain: SolanaChain, + params: ExecuteAppendToLookupTableParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'appendToLookupTable requires authority to be the executing wallet. Use generateUnsignedAppendToLookupTable for vault-owned ALTs and have the vault sign/execute it.', + ) + } + + const tx = await this.buildUnsigned(chain, parsed) + return submit(chain, wallet, tx, this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.test.ts new file mode 100644 index 00000000..e8231117 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.test.ts @@ -0,0 +1,193 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { AddressLookupTableProgram, Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { TOKEN_POOL_PROGRAMS } from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const POOL_PROGRAM = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const FEE_QUOTER = Keypair.generate().publicKey +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stubChain(onGetSlot?: () => void): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getSlot: async () => { + onGetSlot?.() + return 123 + }, + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }), + }, + getTokenPoolConfig: async () => ({ + token: TOKEN, + router: ROUTER, + tokenPoolProgram: POOL_PROGRAM, + }), + _getRouterConfig: async () => ({ feeQuoter: FEE_QUOTER }), + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedCreateLookupTable({ + tokenAddress: TOKEN, + poolProgramAddress: POOL_PROGRAM, + payer: PAYER, + ...opts, + }) +} + +describe('CreateLookupTable (cct/solana)', () => { + describe('generate', () => { + it('builds create + extend ALT instructions', async () => { + const unsigned = await generate() + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 2) + assert.match(unsigned.lookupTableAddress, /^[1-9A-HJ-NP-Za-km-z]+$/) + assert.equal( + unsigned.instructions[0]!.programId.toBase58(), + AddressLookupTableProgram.programId.toBase58(), + ) + assert.equal( + unsigned.instructions[1]!.programId.toBase58(), + AddressLookupTableProgram.programId.toBase58(), + ) + assert.equal( + unsigned.instructions[0]!.keys.find((key) => key.pubkey.toBase58() === PAYER)?.isSigner, + false, + ) + }) + + it('accepts a canonical pool type', async () => { + const unsigned = await SolanaTokenManager.fromChain( + stubChain(), + ).generateUnsignedCreateLookupTable({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + }) + + assert.equal(unsigned.instructions.length, 2) + assert.ok( + unsigned.instructions[1]!.data.includes( + new PublicKey(TOKEN_POOL_PROGRAMS['burn-mint']).toBuffer(), + ), + ) + }) + + it('builds create-only ALT instruction in createEmpty mode', async () => { + const unsigned = await SolanaTokenManager.fromChain( + stubChain(), + ).generateUnsignedCreateLookupTable({ + payer: PAYER, + authority: AUTHORITY, + mode: 'createEmpty', + }) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + assert.match(unsigned.lookupTableAddress, /^[1-9A-HJ-NP-Za-km-z]+$/) + assert.equal( + unsigned.instructions[0]!.programId.toBase58(), + AddressLookupTableProgram.programId.toBase58(), + ) + assert.equal( + unsigned.instructions[0]!.keys.find((key) => key.pubkey.toBase58() === AUTHORITY)?.isSigner, + false, + ) + }) + + it('defaults createEmpty authority to payer', async () => { + const unsigned = await SolanaTokenManager.fromChain( + stubChain(), + ).generateUnsignedCreateLookupTable({ + payer: PAYER, + mode: 'createEmpty', + }) + + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + + it('chunks additional addresses into multiple extend instructions', async () => { + const additionalAddresses = Array.from({ length: 21 }, () => + Keypair.generate().publicKey.toBase58(), + ) + const unsigned = await generate({ additionalAddresses }) + + assert.equal(unsigned.instructions.length, 3) + }) + + it('uses caller-provided authority', async () => { + const unsigned = await generate({ authority: AUTHORITY }) + + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === AUTHORITY)) + }) + + it('rejects ALTs over 256 addresses', async () => { + const additionalAddresses = Array.from({ length: 247 }, () => + Keypair.generate().publicKey.toBase58(), + ) + + await assert.rejects( + () => generate({ additionalAddresses }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'createLookupTable' && + err.context.param === 'additionalAddresses', + ) + }) + }) + + describe('validation', () => { + it('rejects an ambiguous pool reference before the slot RPC', async () => { + let getSlotCalls = 0 + + await assert.rejects( + SolanaTokenManager.fromChain( + stubChain(() => getSlotCalls++), + ).generateUnsignedCreateLookupTable({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + poolProgramAddress: POOL_PROGRAM, + payer: PAYER, + } as never), + CCTParamsInvalidError, + ) + + assert.equal(getSlotCalls, 0) + }) + }) + + describe('execute', () => { + it('rejects signed create+extend when authority is not the wallet', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).createLookupTable({ + tokenAddress: TOKEN, + poolProgramAddress: POOL_PROGRAM, + wallet: WALLET, + authority: AUTHORITY, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'createLookupTable' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.ts new file mode 100644 index 00000000..4f04a684 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.ts @@ -0,0 +1,195 @@ +import { + type PublicKey, + type TransactionInstruction, + AddressLookupTableProgram, +} from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + buildCreateLookupTableInstruction, + deriveCcipLookupTableAddresses, +} from '../../programs/alt.ts' +import type { PoolProgramRef } from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, +} from '../../validate.ts' + +const MAX_ALT_ADDRESSES = 256 +const EXTEND_CHUNK_SIZE = 30 + +type CreateLookupTableMode = 'createAndExtend' | 'createEmpty' + +/** Parameters shared by Solana TokenAdminRegistry `createLookupTable` generation and execution. */ +type CreateLookupTableParams = + | (PoolProgramRef & { + /** Defaults to `createAndExtend`; use `createEmpty` to skip extending the ALT. */ + mode?: Extract + tokenAddress: string + additionalAddresses?: string[] + /** ALT authority. Defaults to payer for unsigned generation and wallet public key for execute. */ + authority?: string + }) + | { + /** Creates an empty ALT without extend instructions. */ + mode: Extract + /** ALT authority. Defaults to payer for unsigned generation and wallet public key for execute. */ + authority?: string + } + +/** Parameters for unsigned Solana lookup table generation. */ +export type GenerateCreateLookupTableParams = SolanaGenerateParams + +type ParsedCreateLookupTableParams = + | { mode: 'createEmpty'; payer: PublicKey; authority: PublicKey } + | { + mode: 'createAndExtend' + payer: PublicKey + authority: PublicKey + tokenMint: PublicKey + poolProgram: PublicKey + additionalAddresses: PublicKey[] + } + +/** Unsigned create lookup table result, including the derived ALT address. */ +export type GenerateCreateLookupTableResult = UnsignedSolanaTx & { + lookupTableAddress: string +} + +/** Parameters for executing Solana TokenAdminRegistry `createLookupTable`. */ +export type ExecuteCreateLookupTableParams = SolanaExecuteParams + +/** Result of executing Solana TokenAdminRegistry `createLookupTable`. */ +export type ExecuteCreateLookupTableResult = TransactionResult & { lookupTableAddress: string } + +/** Builds and submits Solana ALT create instructions, optionally with extend instructions. */ +export class CreateLookupTable extends SolanaOperation< + CreateLookupTableParams, + GenerateCreateLookupTableResult, + ParsedCreateLookupTableParams +> { + readonly name = 'createLookupTable' + + /** Parses params before `buildUnsigned()` performs any RPC. */ + protected override parse(params: GenerateCreateLookupTableParams): ParsedCreateLookupTableParams { + const payer = parsePublicKey(this.name, 'payer', params.payer) + const authority = + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority) + if (params.mode === 'createEmpty') return { mode: 'createEmpty', payer, authority } + + return { + mode: 'createAndExtend', + payer, + authority, + tokenMint: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram: resolvePoolProgram(this.name, params), + additionalAddresses: (params.additionalAddresses ?? []).map((address, i) => + parsePublicKey(this.name, `additionalAddresses[${i}]`, address), + ), + } + } + + /** Builds unsigned ALT create instructions, optionally with extend instructions. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedCreateLookupTableParams, + ): Promise { + const { payer, authority } = opts + + if (opts.mode === 'createEmpty') { + const { instruction: createIx, lookupTableAddress } = buildCreateLookupTableInstruction({ + authority, + payer, + recentSlot: await chain.connection.getSlot('finalized'), + }) + chain.logger.debug( + `${this.name}: mode = createEmpty, lookupTable = ${lookupTableAddress.toBase58()}`, + ) + return { + family: ChainFamily.Solana, + instructions: [createIx], + mainIndex: 0, + lookupTableAddress: lookupTableAddress.toBase58(), + } + } + + const { poolProgram, tokenMint, additionalAddresses } = opts + + const { instruction: createIx, lookupTableAddress } = buildCreateLookupTableInstruction({ + authority, + payer, + recentSlot: await chain.connection.getSlot('finalized'), + }) + + const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { + lookupTableAddress, + tokenMint, + poolProgram, + }) + const addresses = [...ccipAddresses, ...additionalAddresses] + + if (addresses.length > MAX_ALT_ADDRESSES) { + throw new CCTParamsInvalidError( + this.name, + 'additionalAddresses', + `ALT cannot exceed ${MAX_ALT_ADDRESSES} addresses; requested ${addresses.length}`, + ) + } + + const extendIxs: TransactionInstruction[] = [] + for (let i = 0; i < addresses.length; i += EXTEND_CHUNK_SIZE) { + extendIxs.push( + AddressLookupTableProgram.extendLookupTable({ + payer, + authority, + lookupTable: lookupTableAddress, + addresses: addresses.slice(i, i + EXTEND_CHUNK_SIZE), + }), + ) + } + + chain.logger.debug( + `${this.name}: token = ${tokenMint.toBase58()}, lookupTable = ${lookupTableAddress.toBase58()}`, + ) + return { + family: ChainFamily.Solana, + instructions: [createIx, ...extendIxs], + mainIndex: 0, + lookupTableAddress: lookupTableAddress.toBase58(), + } + } + + /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ + override async execute( + chain: SolanaChain, + params: ExecuteCreateLookupTableParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.mode !== 'createEmpty' && params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + "createAndExtend requires authority to be the executing wallet. Use 'createEmpty' mode for vault-owned ALTs.", + ) + } + + const tx = await this.buildUnsigned(chain, parsed) + const hash = await submit(chain, wallet, tx, this.name, computeUnits) + return { ...hash, lookupTableAddress: tx.lookupTableAddress } + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-supported-tokens.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-supported-tokens.test.ts new file mode 100644 index 00000000..b5868427 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-supported-tokens.test.ts @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair } from '@solana/web3.js' + +import { GetSupportedTokens } from './get-supported-tokens.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const OFF_RAMP = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const TOKENS = [Keypair.generate().publicKey.toBase58()] + +describe('GetSupportedTokens (cct/solana)', () => { + describe('query', () => { + it('resolves an OffRamp to the Router and lists configured token mints', async () => { + let resolvedAddress: string | undefined + let supportedTokensRouter: string | undefined + const chain = { + getTokenAdminRegistryFor: async (address: string) => { + resolvedAddress = address + return ROUTER + }, + getSupportedTokens: async (router: string) => { + supportedTokensRouter = router + return TOKENS + }, + } as unknown as SolanaChain + + assert.deepEqual(await new GetSupportedTokens().query(chain, { address: OFF_RAMP }), TOKENS) + assert.equal(resolvedAddress, OFF_RAMP) + assert.equal(supportedTokensRouter, ROUTER) + }) + }) + + describe('validation', () => { + it('rejects an invalid address', async () => { + await assert.rejects( + () => new GetSupportedTokens().query({} as SolanaChain, { address: 'invalid' }), + (error: unknown) => + error instanceof CCTParamsInvalidError && error.context.param === 'address', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-supported-tokens.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-supported-tokens.ts new file mode 100644 index 00000000..4ae3f663 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-supported-tokens.ts @@ -0,0 +1,29 @@ +import type { SolanaChain } from '../../../../solana/index.ts' +import { SolanaQuery } from '../../query.ts' +import { validatePublicKey } from '../../validate.ts' + +/** Parameters for listing tokens configured in a Solana TokenAdminRegistry. */ +export type GetSupportedTokensParams = { + /** + * CCIP contract to resolve the TokenAdminRegistry/Router from — a Router or OffRamp + * address works. + */ + address: string +} + +/** Lists all SPL token mints configured in a TokenAdminRegistry in a single scan; pagination is not supported. */ +export class GetSupportedTokens extends SolanaQuery { + readonly name = 'getSupportedTokens' + + /** Validates the resolution address; nothing to convert for {@link read}. */ + protected prepare(params: GetSupportedTokensParams): GetSupportedTokensParams { + validatePublicKey(this.name, 'address', params.address) + return params + } + + /** Resolves the Router and lists its configured token mints. */ + protected async read(chain: SolanaChain, params: GetSupportedTokensParams): Promise { + const router = await chain.getTokenAdminRegistryFor(params.address) + return chain.getSupportedTokens(router) + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-token-admin-registry.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-token-admin-registry.test.ts new file mode 100644 index 00000000..23756b46 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-token-admin-registry.test.ts @@ -0,0 +1,155 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { BorshAccountsCoder } from '@coral-xyz/anchor' +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js' + +import { + CCIPDataFormatUnsupportedError, + CCIPTokenNotConfiguredError, +} from '../../../../errors/index.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveTokenAdminRegistryPda } from '../../programs/router.ts' + +const ROUTER = Keypair.generate().publicKey +const TOKEN = Keypair.generate().publicKey +const ADMINISTRATOR = Keypair.generate().publicKey +const PENDING_ADMINISTRATOR = Keypair.generate().publicKey +const LOOKUP_TABLE = Keypair.generate().publicKey +const POOL = Keypair.generate().publicKey +const REGISTRY = deriveTokenAdminRegistryPda(ROUTER, TOKEN) + +function registryAccount( + pendingAdministrator = PENDING_ADMINISTRATOR, + poolLookupTable = LOOKUP_TABLE, + supportsAutoDerivation = true, + hasSupportsAutoDerivation = true, +) { + const data = Buffer.alloc(hasSupportsAutoDerivation ? 170 : 169) + BorshAccountsCoder.accountDiscriminator('TokenAdminRegistry').copy(data) + data[8] = 2 + ADMINISTRATOR.toBuffer().copy(data, 9) + pendingAdministrator.toBuffer().copy(data, 41) + poolLookupTable.toBuffer().copy(data, 73) + data[120] = 0x19 // Writable indexes 3, 4, and 7 use the high bits of the first u128 bitmap. + data[136] = 0x20 // Writable index 130 uses the high bits of the second u128 bitmap. + TOKEN.toBuffer().copy(data, 137) + if (hasSupportsAutoDerivation && supportsAutoDerivation) data[169] = 1 + return { data } +} + +function stubChain(account: { data: Buffer } | null = registryAccount()): SolanaChain { + return { + connection: { + getAccountInfo: async (address: PublicKey) => (address.equals(REGISTRY) ? account : null), + getAddressLookupTable: async (address: PublicKey) => ({ + value: address.equals(LOOKUP_TABLE) + ? { + state: { addresses: [PublicKey.default, PublicKey.default, PublicKey.default, POOL] }, + } + : null, + }), + }, + getTokenAdminRegistryFor: async () => ROUTER.toBase58(), + } as unknown as SolanaChain +} + +describe('GetTokenAdminRegistry (cct/solana)', () => { + describe('query', () => { + it('returns configured administrators, lookup table, and writable indexes', async () => { + const config = await SolanaTokenManager.fromChain(stubChain()).getTokenAdminRegistry({ + address: ROUTER.toBase58(), + tokenAddress: TOKEN.toBase58(), + }) + + assert.deepEqual(config, { + mint: TOKEN.toBase58(), + administrator: ADMINISTRATOR.toBase58(), + pendingAdministrator: PENDING_ADMINISTRATOR.toBase58(), + tokenPool: POOL.toBase58(), + lookupTable: LOOKUP_TABLE.toBase58(), + writableIndexes: [3, 4, 7, 130], + supportsAutoDerivation: true, + }) + }) + + it('omits optional fields when unset', async () => { + const config = await SolanaTokenManager.fromChain( + stubChain(registryAccount(PublicKey.default, PublicKey.default, false, false)), + ).getTokenAdminRegistry({ address: ROUTER.toBase58(), tokenAddress: TOKEN.toBase58() }) + + assert.deepEqual(config, { + mint: TOKEN.toBase58(), + administrator: ADMINISTRATOR.toBase58(), + writableIndexes: [3, 4, 7, 130], + supportsAutoDerivation: false, + }) + }) + + it('returns disabled auto derivation setting', async () => { + const config = await SolanaTokenManager.fromChain( + stubChain(registryAccount(PENDING_ADMINISTRATOR, LOOKUP_TABLE, false)), + ).getTokenAdminRegistry({ address: ROUTER.toBase58(), tokenAddress: TOKEN.toBase58() }) + + assert.equal(config.supportsAutoDerivation, false) + }) + + it('omits the system program as pending administrator', async () => { + const config = await SolanaTokenManager.fromChain( + stubChain(registryAccount(SystemProgram.programId)), + ).getTokenAdminRegistry({ address: ROUTER.toBase58(), tokenAddress: TOKEN.toBase58() }) + + assert.equal(config.pendingAdministrator, undefined) + }) + + it('rejects malformed registry data', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain({ data: Buffer.alloc(8) })).getTokenAdminRegistry({ + address: ROUTER.toBase58(), + tokenAddress: TOKEN.toBase58(), + }), + CCIPDataFormatUnsupportedError, + ) + }) + + it('rejects unregistered tokens', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain(null)).getTokenAdminRegistry({ + address: ROUTER.toBase58(), + tokenAddress: TOKEN.toBase58(), + }), + CCIPTokenNotConfiguredError, + ) + }) + }) + + describe('validation', () => { + it('rejects an invalid router address', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).getTokenAdminRegistry({ + address: 'invalid', + tokenAddress: TOKEN.toBase58(), + }), + (error: unknown) => + error instanceof CCTParamsInvalidError && error.context.param === 'address', + ) + }) + + it('rejects an invalid token address', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).getTokenAdminRegistry({ + address: ROUTER.toBase58(), + tokenAddress: 'invalid', + }), + (error: unknown) => + error instanceof CCTParamsInvalidError && error.context.param === 'tokenAddress', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-token-admin-registry.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-token-admin-registry.ts new file mode 100644 index 00000000..463ddbbf --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-token-admin-registry.ts @@ -0,0 +1,67 @@ +import { PublicKey } from '@solana/web3.js' + +import type { RegistryTokenConfig } from '../../../../chain.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { getTokenAdminRegistryConfig } from '../../../../solana/token-admin-registry.ts' +import { SolanaQuery } from '../../query.ts' +import { parsePublicKey, validatePublicKey } from '../../validate.ts' + +/** Parameters for reading a Solana TokenAdminRegistry configuration. */ +export type GetTokenAdminRegistryParams = { + /** + * CCIP contract to resolve the TokenAdminRegistry/Router from — a Router or OffRamp + * address works. + */ + address: string + /** SPL token mint registered with the Router. */ + tokenAddress: string +} + +/** Configuration stored in a Solana TokenAdminRegistry account. */ +export type GetTokenAdminRegistryResult = RegistryTokenConfig & { + mint: string + lookupTable?: string + writableIndexes: number[] + supportsAutoDerivation: boolean +} + +/** {@link GetTokenAdminRegistryParams} with its mint resolved to a public key. */ +type ParsedGetTokenAdminRegistryParams = GetTokenAdminRegistryParams & { + tokenMint: PublicKey +} + +/** Reads a token's TokenAdminRegistry account. */ +export class GetTokenAdminRegistry extends SolanaQuery< + GetTokenAdminRegistryParams, + GetTokenAdminRegistryResult, + ParsedGetTokenAdminRegistryParams +> { + readonly name = 'getTokenAdminRegistry' + + /** Converts the mint; `address` stays a string for the Router lookup in {@link read}. */ + protected prepare(params: GetTokenAdminRegistryParams): ParsedGetTokenAdminRegistryParams { + validatePublicKey(this.name, 'address', params.address) + return { ...params, tokenMint: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress) } + } + + /** Reads and serializes the TokenAdminRegistry account. */ + protected async read( + chain: SolanaChain, + params: ParsedGetTokenAdminRegistryParams, + ): Promise { + const router = new PublicKey(await chain.getTokenAdminRegistryFor(params.address)) + const config = await getTokenAdminRegistryConfig(chain.connection, router, params.tokenMint) + + return { + mint: config.mint.toBase58(), + administrator: config.administrator.toBase58(), + ...(config.pendingAdministrator && { + pendingAdministrator: config.pendingAdministrator.toBase58(), + }), + ...(config.tokenPool && { tokenPool: config.tokenPool.toBase58() }), + ...(config.lookupTable && { lookupTable: config.lookupTable.toBase58() }), + writableIndexes: config.writableIndexes, + supportsAutoDerivation: config.supportsAutoDerivation, + } + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts new file mode 100644 index 00000000..0437d089 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -0,0 +1,8 @@ +export * from './accept-admin.ts' +export * from './append-to-lookup-table.ts' +export * from './create-lookup-table.ts' +export * from './get-supported-tokens.ts' +export * from './get-token-admin-registry.ts' +export * from './register-admin.ts' +export * from './set-pool.ts' +export * from './transfer-admin.ts' diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/register-admin.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/register-admin.test.ts new file mode 100644 index 00000000..5ffdb70a --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/register-admin.test.ts @@ -0,0 +1,194 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { describe, it } from 'node:test' + +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveRouterConfigPda, deriveTokenAdminRegistryPda } from '../../programs/router.ts' + +const TOKEN = Keypair.generate().publicKey +const MINT_AUTHORITY = Keypair.generate().publicKey +const ADDRESS = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const CCIP_ADMIN = Keypair.generate().publicKey +const ADMINISTRATOR = Keypair.generate().publicKey +const CONFIG = deriveRouterConfigPda(new PublicKey(ROUTER)) +const TOKEN_ADMIN_REGISTRY = deriveTokenAdminRegistryPda(new PublicKey(ROUTER), TOKEN) +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function configAccount() { + const data = Buffer.alloc(210) + createHash('sha256').update('account:Config').digest().copy(data, 0, 0, 8) + data[8] = 1 + CCIP_ADMIN.toBuffer().copy(data, 18) + return { data, executable: false, lamports: 1, owner: new PublicKey(ROUTER), rentEpoch: 0 } +} + +function mintAccount(mintAuthority: PublicKey | null = MINT_AUTHORITY) { + const data = Buffer.alloc(82) + if (mintAuthority) { + data.writeUInt32LE(1, 0) + mintAuthority.toBuffer().copy(data, 4) + } + data[44] = 9 + data[45] = 1 + return { data, executable: false, lamports: 1, owner: TOKEN_PROGRAM_ID, rentEpoch: 0 } +} + +function stubChain( + registered = false, + mintAuthority: PublicKey | null = MINT_AUTHORITY, + configAvailable = true, +): SolanaChain { + const getAccountInfo = async (address: PublicKey) => { + if (address.equals(TOKEN)) return mintAccount(mintAuthority) + if (address.equals(TOKEN_ADMIN_REGISTRY)) return registered ? mintAccount() : null + if (address.equals(CONFIG)) return configAvailable ? configAccount() : null + return assert.fail('unexpected account lookup') + } + + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo, + getAccountInfoAndContext: async (address: PublicKey) => ({ + context: { slot: 0 }, + value: await getAccountInfo(address), + }), + }, + getTokenAdminRegistryFor: async () => ROUTER, + } as unknown as SolanaChain +} + +function generate(opts = {}, registered = false, mintAuthority: PublicKey | null = MINT_AUTHORITY) { + return SolanaTokenManager.fromChain( + stubChain(registered, mintAuthority), + ).generateUnsignedRegisterAdmin({ + tokenAddress: TOKEN.toBase58(), + address: ADDRESS, + payer: PAYER, + authority: MINT_AUTHORITY.toBase58(), + ...opts, + }) +} + +describe('RegisterAdmin (cct/solana)', () => { + describe('generate', () => { + it('builds owner registration with the mint authority as proposed admin', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction.programId.toBase58(), ROUTER) + assert.equal(instruction.data.subarray(0, 8).toString('hex'), 'af51a0f6ce841216') + assert.ok(instruction.keys.some((key) => key.pubkey.equals(MINT_AUTHORITY))) + assert.deepEqual(instruction.data.subarray(-32), MINT_AUTHORITY.toBuffer()) + }) + + it('builds the CCIP-admin registration instruction without a mint authority', async () => { + const ccipAdmin = await generate( + { + registrationMethod: 'ccip-admin', + authority: CCIP_ADMIN.toBase58(), + administrator: ADMINISTRATOR.toBase58(), + }, + false, + null, + ) + + assert.equal( + ccipAdmin.instructions[0]!.data.subarray(0, 8).toString('hex'), + 'da258b6b8ee433db', + ) + assert.deepEqual(ccipAdmin.instructions[0]!.data.subarray(-32), ADMINISTRATOR.toBuffer()) + }) + }) + + describe('validation', () => { + it('rejects owner registration when authority is not the mint authority', async () => { + await assert.rejects( + () => generate({ authority: PAYER }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'authority', + ) + }) + + it('rejects a token that is already registered', async () => { + await assert.rejects( + () => generate({}, true), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'tokenAddress', + ) + }) + + it('requires an administrator for CCIP-admin registration without a mint authority', async () => { + await assert.rejects( + () => + generate( + { registrationMethod: 'ccip-admin', authority: CCIP_ADMIN.toBase58() }, + false, + null, + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'administrator', + ) + }) + + it('rejects a missing Router config with a typed error', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain( + stubChain(false, MINT_AUTHORITY, false), + ).generateUnsignedRegisterAdmin({ + tokenAddress: TOKEN.toBase58(), + address: ADDRESS, + registrationMethod: 'ccip-admin', + payer: PAYER, + authority: CCIP_ADMIN.toBase58(), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'address', + ) + }) + + it('rejects CCIP-admin registration when authority is not the Router CCIP admin', async () => { + await assert.rejects( + () => generate({ registrationMethod: 'ccip-admin' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'authority', + ) + }) + + it('rejects an unknown registration method before RPC', async () => { + await assert.rejects( + () => generate({ registrationMethod: 'other' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'registrationMethod', + ) + }) + }) + + describe('execute', () => { + it('rejects an authority that differs from the executing wallet', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).registerAdmin({ + tokenAddress: TOKEN.toBase58(), + address: ADDRESS, + registrationMethod: 'owner', + authority: MINT_AUTHORITY.toBase58(), + wallet: WALLET, + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/register-admin.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/register-admin.ts new file mode 100644 index 00000000..3df93173 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/register-admin.ts @@ -0,0 +1,251 @@ +import { unpackMint } from '@solana/spl-token' +import { type TransactionInstruction, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { resolveTokenMint } from '../../../../solana/utils.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { submit } from '../../submit.ts' +import { parsePublicKey, validateAuthorityMatchesWallet } from '../../validate.ts' + +/** Authorization paths used to register a token in the TokenAdminRegistry. */ +const REGISTER_ADMIN_METHODS = { + OWNER: 'owner', + CCIP_ADMIN: 'ccip-admin', +} as const + +/** Authorization path used to register a token in the TokenAdminRegistry. */ +export type RegisterAdminMethod = + (typeof REGISTER_ADMIN_METHODS)[keyof typeof REGISTER_ADMIN_METHODS] + +type RegisterAdminParams = { + /** Token mint to register. The proposed administrator remains pending until accepted. */ + tokenAddress: string + /** + * CCIP contract to resolve the TokenAdminRegistry/Router from — a Router or OffRamp + * address works. + */ + address: string + /** Selects registration authority; defaults to `owner`. */ + registrationMethod?: RegisterAdminMethod + /** Registry administrator to propose. Defaults to the mint authority when present. */ + administrator?: string + /** + * Registration authority. Defaults to `payer` for single-signer transactions. + * Multisig/Squads flows should pass the mint or CCIP admin/vault authority explicitly. + */ + authority?: string +} + +/** Parameters for unsigned Solana token registration generation. */ +export type GenerateRegisterAdminParams = SolanaGenerateParams + +type ParsedRegisterAdminParams = { + tokenMint: PublicKey + address: PublicKey + payer: PublicKey + authority: PublicKey + administrator?: PublicKey + method: RegisterAdminMethod +} + +/** Unsigned Solana token registration result. */ +export type GenerateRegisterAdminResult = UnsignedSolanaTx + +/** Parameters for executing Solana token registration. */ +export type ExecuteRegisterAdminParams = SolanaExecuteParams + +/** Result of executing Solana token registration. */ +export type ExecuteRegisterAdminResult = TransactionResult + +type RegisterAdminAccounts = { + config: PublicKey + tokenAdminRegistry: PublicKey + mint: PublicKey + authority: PublicKey +} + +type RouterProgram = ReturnType + +async function buildOwnerInstruction( + program: RouterProgram, + accounts: RegisterAdminAccounts, + mintAuthority: PublicKey | null, + administrator: PublicKey, +): Promise { + if (!mintAuthority) { + throw new CCTParamsInvalidError( + 'registerAdmin', + 'tokenAddress', + 'token mint has no mint authority; use ccip-admin with administrator', + ) + } + if (!accounts.authority.equals(mintAuthority)) { + throw new CCTParamsInvalidError( + 'registerAdmin', + 'authority', + 'must match the token mint authority', + ) + } + return program.methods.ownerProposeAdministrator(administrator).accounts(accounts).instruction() +} + +async function buildCcipAdminInstruction( + program: RouterProgram, + accounts: RegisterAdminAccounts, + administrator: PublicKey, +): Promise { + let routerConfig + try { + routerConfig = await program.account.config.fetch(accounts.config) + } catch (cause) { + throw new CCTParamsInvalidError( + 'registerAdmin', + 'address', + 'Router config could not be fetched', + { + cause: cause instanceof Error ? cause : undefined, + }, + ) + } + + if (!accounts.authority.equals(routerConfig.owner)) { + throw new CCTParamsInvalidError( + 'registerAdmin', + 'authority', + 'must match the Router CCIP admin', + ) + } + return program.methods + .ccipAdminProposeAdministrator(administrator) + .accounts(accounts) + .instruction() +} + +/** Registers a token through either its mint authority or the Router CCIP admin. */ +export class RegisterAdmin extends SolanaOperation< + RegisterAdminParams, + UnsignedSolanaTx, + ParsedRegisterAdminParams +> { + readonly name = 'registerAdmin' + + /** Parses all caller-supplied parameters before RPC. */ + protected override parse(params: GenerateRegisterAdminParams): ParsedRegisterAdminParams { + if ( + params.registrationMethod !== undefined && + !Object.values(REGISTER_ADMIN_METHODS).includes(params.registrationMethod) + ) { + throw new CCTParamsInvalidError( + this.name, + 'registrationMethod', + 'must be owner or ccip-admin', + ) + } + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenMint: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + address: parsePublicKey(this.name, 'address', params.address), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + ...(params.administrator !== undefined && { + administrator: parsePublicKey(this.name, 'administrator', params.administrator), + }), + method: params.registrationMethod ?? REGISTER_ADMIN_METHODS.OWNER, + } + } + + /** Builds an unsigned token registration instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedRegisterAdminParams, + ): Promise { + const router = new PublicKey(await chain.getTokenAdminRegistryFor(opts.address.toBase58())) + const { tokenMint, payer, authority, method } = opts + + const mintAccount = await resolveTokenMint(chain.connection, tokenMint) + const { mintAuthority } = unpackMint(tokenMint, mintAccount, mintAccount.owner) + const administrator = opts.administrator ?? mintAuthority + const config = deriveRouterConfigPda(router) + const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, tokenMint) + if (await chain.connection.getAccountInfo(tokenAdminRegistry)) { + throw new CCTParamsInvalidError( + this.name, + 'tokenAddress', + 'a registry entry already exists for this token (possibly pending admin acceptance) — use acceptAdmin/setPool instead of registering again', + ) + } + + const program = createRouterProgram(chain, router, payer) + const accounts = { config, tokenAdminRegistry, mint: tokenMint, authority } + + if (!administrator) { + throw new CCTParamsInvalidError( + this.name, + 'administrator', + 'is required when the mint has no mint authority', + ) + } + + const instructions: TransactionInstruction[] = [] + switch (method) { + case REGISTER_ADMIN_METHODS.OWNER: { + const ownerIx = await buildOwnerInstruction(program, accounts, mintAuthority, administrator) + instructions.push(ownerIx) + break + } + case REGISTER_ADMIN_METHODS.CCIP_ADMIN: { + const ccipAdminIx = await buildCcipAdminInstruction(program, accounts, administrator) + instructions.push(ccipAdminIx) + break + } + default: + throw new CCTParamsInvalidError( + this.name, + 'registrationMethod', + 'must be owner or ccip-admin', + ) + } + + chain.logger.debug( + `${this.name}: method = ${method}, router = ${router.toBase58()}, token = ${tokenMint.toBase58()}`, + ) + + return { family: ChainFamily.Solana, instructions, mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the registration authority. */ + override async execute( + chain: SolanaChain, + params: ExecuteRegisterAdminParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'registerAdmin requires authority to be the executing wallet. Use generateUnsignedRegisterAdmin for externally signed transactions.', + ) + } + + const tx = await this.buildUnsigned(chain, parsed) + return submit(chain, wallet, tx, this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.test.ts new file mode 100644 index 00000000..8a44bea7 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.test.ts @@ -0,0 +1,128 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' + +const BLOCKHASH = PublicKey.default.toBase58() +const TOKEN = Keypair.generate().publicKey.toBase58() +const ADDRESS = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const POOL_LOOKUP_TABLE = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() + +function stubChain(router = ROUTER, onAddress?: (address: string) => void): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: () => assert.fail('should not RPC before validation'), + getLatestBlockhash: async () => ({ blockhash: BLOCKHASH, lastValidBlockHeight: 0 }), + }, + getTokenAdminRegistryFor: async (address: string) => { + onAddress?.(address) + return router + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedSetPool({ + tokenAddress: TOKEN, + address: ADDRESS, + poolLookupTableAddress: POOL_LOOKUP_TABLE, + payer: PAYER, + ...opts, + }) +} + +describe('SetPool (cct/solana)', () => { + describe('generate', () => { + it('builds unsigned setPool instruction with default writable indexes and authority', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + assert.equal(instruction.programId.toBase58(), ROUTER) + assert.equal(instruction.data.toString('hex'), '771e0eb473e1a7ee03000000030407') + assert.ok(instruction.keys.some((key) => key.pubkey.toBase58() === TOKEN)) + assert.ok(instruction.keys.some((key) => key.pubkey.toBase58() === POOL_LOOKUP_TABLE)) + assert.ok(instruction.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + + it('uses caller-provided writable indexes', async () => { + const unsigned = await generate({ writableIndexes: [3, 4, 7, 9] }) + + assert.equal( + unsigned.instructions[0]!.data.toString('hex'), + '771e0eb473e1a7ee0400000003040709', + ) + }) + + it('resolves the router from address', async () => { + let requestedAddress: string | undefined + const cct = SolanaTokenManager.fromChain( + stubChain(ROUTER, (address) => (requestedAddress = address)), + ) + + const unsigned = await cct.generateUnsignedSetPool({ + tokenAddress: TOKEN, + address: ADDRESS, + poolLookupTableAddress: POOL_LOOKUP_TABLE, + payer: PAYER, + }) + + assert.equal(requestedAddress, ADDRESS) + assert.equal(unsigned.instructions[0]!.programId.toBase58(), ROUTER) + }) + + it('uses caller-provided authority', async () => { + const unsigned = await generate({ authority: AUTHORITY }) + + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === AUTHORITY)) + }) + }) + + describe('validation', () => { + it('rejects invalid writable indexes before resolving the router', async () => { + let routerLookups = 0 + + await assert.rejects( + SolanaTokenManager.fromChain( + stubChain(ROUTER, () => routerLookups++), + ).generateUnsignedSetPool({ + tokenAddress: TOKEN, + address: ADDRESS, + poolLookupTableAddress: POOL_LOOKUP_TABLE, + payer: PAYER, + writableIndexes: [], + }), + CCTParamsInvalidError, + ) + + assert.equal(routerLookups, 0) + }) + }) + + describe('execute', () => { + it('rejects an invalid wallet before generating instructions', async () => { + await assert.rejects( + SolanaTokenManager.fromChain(stubChain()).setPool({ + tokenAddress: TOKEN, + address: ADDRESS, + poolLookupTableAddress: POOL_LOOKUP_TABLE, + wallet: {}, + }), + CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.ts new file mode 100644 index 00000000..bfcb5a0f --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.ts @@ -0,0 +1,122 @@ +import { Buffer } from 'buffer' + +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { parsePublicKey, validateWritableIndexes } from '../../validate.ts' + +/** Standard BurnMint/LockRelease pool ALT writable positions. */ +export const DEFAULT_WRITABLE_INDEXES = [3, 4, 7] as const + +/** Parameters shared by Solana TokenAdminRegistry `setPool` generation and execution. */ +type SetPoolParams = { + tokenAddress: string + /** + * CCIP contract to resolve the TokenAdminRegistry/Router from — the registry itself, + * a Router, OnRamp, OffRamp, or TokenPool address all work. + */ + address: string + /** The pool's Address Lookup Table address, produced by the `createLookupTable` op. */ + poolLookupTableAddress: string + /** + * Positions in the pool's own Address Lookup Table the Router marks writable during a + * transfer. Defaults to {@link DEFAULT_WRITABLE_INDEXES} for standard BurnMint/LockRelease + * pools; custom pools with extra accounts MUST extend this or the pool CPI gets wrong + * write-permissions and fails at execution. Each entry is a byte (0–255). + */ + writableIndexes?: number[] + /** + * Token admin authority. Defaults to `payer` for single-signer transactions. + * Multisig/Squads flows should pass the admin/vault authority explicitly. + */ + authority?: string +} + +/** Parameters for unsigned Solana TokenAdminRegistry `setPool` generation. */ +export type GenerateSetPoolParams = SolanaGenerateParams + +type ParsedSetPoolParams = { + tokenMint: PublicKey + address: PublicKey + lookupTable: PublicKey + payer: PublicKey + authority: PublicKey + writableIndexes: number[] +} + +/** Unsigned Solana TokenAdminRegistry `setPool` result. */ +export type GenerateSetPoolResult = UnsignedSolanaTx + +/** Parameters for executing Solana TokenAdminRegistry `setPool`. */ +export type ExecuteSetPoolParams = SolanaExecuteParams + +/** Result of executing Solana TokenAdminRegistry `setPool`. */ +export type ExecuteSetPoolResult = TransactionResult + +/** Solana TokenAdminRegistry `setPool` operation. */ +export class SetPool extends SolanaOperation { + readonly name = 'setPool' + + /** Parses all public keys before any RPC. */ + protected override parse(params: GenerateSetPoolParams): ParsedSetPoolParams { + validateWritableIndexes(this.name, 'writableIndexes', params.writableIndexes) + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenMint: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + address: parsePublicKey(this.name, 'address', params.address), + lookupTable: parsePublicKey( + this.name, + 'poolLookupTableAddress', + params.poolLookupTableAddress, + ), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + writableIndexes: params.writableIndexes ?? [...DEFAULT_WRITABLE_INDEXES], + } + } + + /** Builds the unsigned Solana `setPool` instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedSetPoolParams, + ): Promise { + const router = new PublicKey(await chain.getTokenAdminRegistryFor(opts.address.toBase58())) + const { tokenMint, payer, authority, lookupTable } = opts + + const routerProgram = createRouterProgram(chain, router, payer) + const config = deriveRouterConfigPda(router) + const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, tokenMint) + + const instruction = await routerProgram.methods + .setPool(Buffer.from(opts.writableIndexes)) + .accounts({ + config, + tokenAdminRegistry, + mint: tokenMint, + poolLookuptable: lookupTable, + authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: router = ${router.toBase58()}, token = ${tokenMint.toBase58()}, lookupTable = ${lookupTable.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin.test.ts new file mode 100644 index 00000000..0c51355b --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin.test.ts @@ -0,0 +1,157 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import type { GenerateTransferAdminParams } from './transfer-admin.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveRouterConfigPda, deriveTokenAdminRegistryPda } from '../../programs/router.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const ADDRESS = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const NEW_ADMIN = Keypair.generate().publicKey.toBase58() +const CURRENT_ADMIN = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stubChain( + administrator = CURRENT_ADMIN, + onAddress?: (address: string) => void, + pendingAdministrator?: string, +): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + getTokenAdminRegistryFor: async (address: string) => { + onAddress?.(address) + return ROUTER + }, + getRegistryTokenConfig: async () => ({ administrator, pendingAdministrator }), + } as unknown as SolanaChain +} + +function generate(opts: Partial = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedTransferAdmin({ + tokenAddress: TOKEN, + address: ADDRESS, + newAdmin: NEW_ADMIN, + payer: PAYER, + authority: CURRENT_ADMIN, + ...opts, + }) +} + +describe('TransferAdmin (cct/solana)', () => { + describe('generate', () => { + it('builds an unsigned transfer admin instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction.programId.toBase58(), ROUTER) + assert.equal(instruction.data.subarray(0, 8).toString('hex'), 'b262cbb5cb6b6a0e') + assert.deepEqual(instruction.data.subarray(8), new PublicKey(NEW_ADMIN).toBuffer()) + assert.deepEqual( + instruction.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveRouterConfigPda(new PublicKey(ROUTER)).toBase58(), + isSigner: false, + isWritable: false, + }, + { + pubkey: deriveTokenAdminRegistryPda( + new PublicKey(ROUTER), + new PublicKey(TOKEN), + ).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: TOKEN, isSigner: false, isWritable: false }, + { pubkey: CURRENT_ADMIN, isSigner: true, isWritable: true }, + ], + ) + }) + + it('resolves the router from address', async () => { + let requestedAddress: string | undefined + const cct = SolanaTokenManager.fromChain( + stubChain(CURRENT_ADMIN, (address) => (requestedAddress = address)), + ) + + await cct.generateUnsignedTransferAdmin({ + tokenAddress: TOKEN, + address: ADDRESS, + newAdmin: NEW_ADMIN, + payer: PAYER, + authority: CURRENT_ADMIN, + }) + + assert.equal(requestedAddress, ADDRESS) + }) + }) + + describe('validation', () => { + it('rejects an authority that is not the current administrator', async () => { + await assert.rejects( + () => generate({ authority: PAYER }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'authority', + ) + }) + + it('requires a pending admin to accept the initial registration before transferring', async () => { + const cct = SolanaTokenManager.fromChain( + stubChain(PublicKey.default.toBase58(), undefined, CURRENT_ADMIN), + ) + + await assert.rejects( + () => + cct.generateUnsignedTransferAdmin({ + tokenAddress: TOKEN, + address: ADDRESS, + newAdmin: NEW_ADMIN, + payer: PAYER, + authority: CURRENT_ADMIN, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'authority' && + typeof err.context.reason === 'string' && + err.context.reason.includes('still pending acceptance'), + ) + }) + }) + + describe('execute', () => { + it('requires the current admin to be the executing wallet', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).transferAdmin({ + tokenAddress: TOKEN, + address: ADDRESS, + newAdmin: NEW_ADMIN, + authority: CURRENT_ADMIN, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'authority' && + typeof err.context.reason === 'string' && + err.context.reason.includes('requires authority to be the executing wallet'), + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin.ts new file mode 100644 index 00000000..3b125e1a --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin.ts @@ -0,0 +1,132 @@ +import { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { submit } from '../../submit.ts' +import { parsePublicKey, validateAuthorityMatchesWallet } from '../../validate.ts' + +/** Parameters shared by Solana TokenAdminRegistry `transferAdmin` generation and execution. */ +type TransferAdminParams = { + tokenAddress: string + /** + * CCIP contract to resolve the TokenAdminRegistry/Router from — a Router or OffRamp + * address works. + */ + address: string + /** The administrator proposed to accept the token's registry admin role. */ + newAdmin: string + /** Current token admin. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +/** Parameters for unsigned Solana TokenAdminRegistry `transferAdmin` generation. */ +export type GenerateTransferAdminParams = SolanaGenerateParams + +type ParsedTransferAdminParams = { + tokenMint: PublicKey + address: PublicKey + newAdmin: PublicKey + payer: PublicKey + authority: PublicKey +} + +/** Unsigned Solana TokenAdminRegistry `transferAdmin` result. */ +export type GenerateTransferAdminResult = UnsignedSolanaTx + +/** Parameters for executing Solana TokenAdminRegistry `transferAdmin`. */ +export type ExecuteTransferAdminParams = SolanaExecuteParams + +/** Result of executing Solana TokenAdminRegistry `transferAdmin`. */ +export type ExecuteTransferAdminResult = TransactionResult + +/** Transfers a TokenAdminRegistry administrator role. The proposed admin must accept separately. */ +export class TransferAdmin extends SolanaOperation< + TransferAdminParams, + UnsignedSolanaTx, + ParsedTransferAdminParams +> { + readonly name = 'transferAdmin' + + /** Parses all public keys before any RPC. */ + protected override parse(params: GenerateTransferAdminParams): ParsedTransferAdminParams { + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenMint: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + address: parsePublicKey(this.name, 'address', params.address), + newAdmin: parsePublicKey(this.name, 'newAdmin', params.newAdmin), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + } + } + + /** Builds the unsigned instruction after confirming the caller is the current admin. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedTransferAdminParams, + ): Promise { + const { tokenMint, payer, authority, newAdmin } = opts + const router = new PublicKey(await chain.getTokenAdminRegistryFor(opts.address.toBase58())) + const tokenConfig = await chain.getRegistryTokenConfig(router.toBase58(), tokenMint.toBase58()) + + if (!new PublicKey(tokenConfig.administrator).equals(authority)) { + const pending = tokenConfig.pendingAdministrator + throw new CCTParamsInvalidError( + this.name, + 'authority', + PublicKey.default.toBase58() === tokenConfig.administrator && pending + ? `registration for this token is still pending acceptance by ${pending}; the pending administrator must accept the admin role first — this operation only transfers an accepted role` + : `must be the current token administrator (${tokenConfig.administrator})`, + ) + } + + const instruction = await createRouterProgram(chain, router, payer) + .methods.transferAdminRoleTokenAdminRegistry(newAdmin) + .accounts({ + config: deriveRouterConfigPda(router), + tokenAdminRegistry: deriveTokenAdminRegistryPda(router, tokenMint), + mint: tokenMint, + authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: router = ${router.toBase58()}, token = ${tokenMint.toBase58()}, newAdmin = ${newAdmin.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the current admin wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteTransferAdminParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'transferAdmin requires authority to be the executing wallet. Use generateUnsignedTransferAdmin for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/accept-ownership.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/accept-ownership.test.ts new file mode 100644 index 00000000..f1c7b0a6 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/accept-ownership.test.ts @@ -0,0 +1,203 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { BorshAccountsCoder } from '@coral-xyz/anchor' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveTokenPoolConfigPda, resolveTokenPoolProgram } from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stateData(proposedOwner = AUTHORITY): Buffer { + const key = PublicKey.default.toBuffer() + return Buffer.concat([ + BorshAccountsCoder.accountDiscriminator('State'), + Buffer.from([1]), + key, + new PublicKey(TOKEN).toBuffer(), + Buffer.from([6]), + key, + key, + key, + new PublicKey(proposedOwner).toBuffer(), + key, + key, + key, + key, + key, + Buffer.from([0, 0]), + Buffer.alloc(4), + key, + ]) +} + +function chain(proposedOwner = AUTHORITY): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async () => ({ owner: PublicKey.default, data: stateData(proposedOwner) }), + }, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(WALLET.publicKey.toBase58()), + connection: { + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + getAccountInfo: async () => ({ + owner: PublicKey.default, + data: stateData(WALLET.publicKey.toBase58()), + }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(chain()).generateUnsignedAcceptOwnership({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + ...opts, + }) +} + +describe('AcceptOwnership (cct/solana)', () => { + describe('generate', () => { + it('builds the ownership-acceptance instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('burn-mint') + const decoded = tokenPoolCoder.instruction.decode(instruction!.data) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction!.programId.toBase58(), poolProgram.toBase58()) + assert.deepEqual( + instruction!.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: TOKEN, isSigner: false, isWritable: false }, + { pubkey: AUTHORITY, isSigner: true, isWritable: false }, + ], + ) + assert.ok(decoded) + assert.equal(decoded.name, 'acceptOwnership') + }) + + it('defaults authority to payer', async () => { + const unsigned = await SolanaTokenManager.fromChain( + chain(PAYER), + ).generateUnsignedAcceptOwnership({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + }) + + assert.equal(unsigned.instructions[0]!.keys[2]!.pubkey.toBase58(), PAYER) + }) + + it('supports a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await generate({ poolType: undefined, poolProgramAddress }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects an authority that is not the proposed owner', async () => { + await assert.rejects( + () => generate({ authority: PAYER }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'authority' && + err.message.includes('must be the proposed owner'), + ) + }) + + it('rejects when there is no proposed owner', async () => { + const cct = SolanaTokenManager.fromChain(chain(PublicKey.default.toBase58())) + + await assert.rejects( + () => + cct.generateUnsignedAcceptOwnership({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'authority' && + err.message.includes('no proposed owner'), + ) + }) + + it('rejects invalid public keys', async () => { + for (const [opts, param] of [ + [{ tokenAddress: 'invalid' }, 'tokenAddress'], + [{ authority: 'invalid' }, 'authority'], + ]) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).acceptOwnership({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed acceptance', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).acceptOwnership({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + authority: AUTHORITY, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'acceptOwnership' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/accept-ownership.ts b/ccip-sdk/src/cct/solana/token-pool/operations/accept-ownership.ts new file mode 100644 index 00000000..f0fc111c --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/accept-ownership.ts @@ -0,0 +1,125 @@ +import { PublicKey } from '@solana/web3.js' + +import { GetTokenPoolState } from './get-token-pool-state.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type PoolProgramRef, + createTokenPoolProgram, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, +} from '../../validate.ts' + +/** Parameters shared by Solana token pool ownership-acceptance generation and execution. */ +type AcceptOwnershipParams = PoolProgramRef & { + /** Token mint address managed by the pool. */ + tokenAddress: string + /** Proposed pool owner accepting ownership. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedAcceptOwnershipParams = { + tokenAddress: PublicKey + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey +} + +/** Parameters for unsigned Solana token pool ownership acceptance. */ +export type GenerateAcceptOwnershipParams = SolanaGenerateParams + +/** Unsigned Solana token pool ownership acceptance result. */ +export type GenerateAcceptOwnershipResult = UnsignedSolanaTx + +/** Parameters for executing Solana token pool ownership acceptance. */ +export type ExecuteAcceptOwnershipParams = SolanaExecuteParams + +/** Result of executing Solana token pool ownership acceptance. */ +export type ExecuteAcceptOwnershipResult = TransactionResult + +/** Accepts pending ownership of a Solana token pool. */ +export class AcceptOwnership extends SolanaOperation< + AcceptOwnershipParams, + UnsignedSolanaTx, + ParsedAcceptOwnershipParams +> { + readonly name = 'acceptOwnership' + + /** Parses public keys and defaults authority to payer without mutating caller params. */ + protected override parse(params: GenerateAcceptOwnershipParams): ParsedAcceptOwnershipParams { + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram: resolvePoolProgram(this.name, params), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + } + } + + /** Confirms the authority is the proposed owner, then builds the unsigned `acceptOwnership` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedAcceptOwnershipParams, + ): Promise { + const { config } = await new GetTokenPoolState().query(chain, { + tokenAddress: opts.tokenAddress.toBase58(), + poolProgramAddress: opts.poolProgram.toBase58(), + }) + const proposedOwner = new PublicKey(config.proposedOwner) + if (proposedOwner.equals(PublicKey.default)) { + throw new CCTParamsInvalidError(this.name, 'authority', 'no proposed owner') + } + if (!proposedOwner.equals(opts.authority)) { + throw new CCTParamsInvalidError(this.name, 'authority', 'must be the proposed owner') + } + + const instruction = await createTokenPoolProgram(chain, opts.poolProgram, opts.payer) + .methods.acceptOwnership() + .accountsStrict({ + state: deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress), + mint: opts.tokenAddress, + authority: opts.authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the proposed owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteAcceptOwnershipParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'acceptOwnership requires authority to be the executing wallet. Use generateUnsignedAcceptOwnership for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/append-remote-pool-addresses.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/append-remote-pool-addresses.test.ts new file mode 100644 index 00000000..894b44e9 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/append-remote-pool-addresses.test.ts @@ -0,0 +1,172 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { + deriveTokenPoolChainConfigPda, + deriveTokenPoolConfigPda, + resolveTokenPoolProgram, +} from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const SELECTOR = 5009297550715157269n +const REMOTE_POOLS = ['0x1234567890abcdef1234567890abcdef12345678', '0xaabbccdd'] +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function chain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(), + connection: { + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(chain()).generateUnsignedAppendRemotePoolAddresses({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + remoteChainSelector: SELECTOR, + remotePoolAddresses: REMOTE_POOLS, + ...opts, + }) +} + +describe('AppendRemotePoolAddresses (cct/solana)', () => { + describe('generate', () => { + it('builds the append-remote-pool-addresses instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('burn-mint') + const decoded = tokenPoolCoder.instruction.decode(instruction!.data) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction!.programId.toBase58(), poolProgram.toBase58()) + assert.deepEqual( + instruction!.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)).toBase58(), + isSigner: false, + isWritable: false, + }, + { + pubkey: deriveTokenPoolChainConfigPda( + poolProgram, + SELECTOR, + new PublicKey(TOKEN), + ).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: AUTHORITY, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId.toBase58(), isSigner: false, isWritable: false }, + ], + ) + assert.ok(decoded) + assert.equal(decoded.name, 'appendRemotePoolAddresses') + const data = decoded.data as { + remoteChainSelector: { toString(): string } + mint: PublicKey + addresses: { address: Buffer }[] + } + assert.equal(data.remoteChainSelector.toString(), SELECTOR.toString()) + assert.equal(data.mint.toBase58(), TOKEN) + assert.deepEqual( + data.addresses.map(({ address }) => address), + REMOTE_POOLS.map((address) => Buffer.from(address.slice(2), 'hex')), + ) + }) + + it('uses a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await generate({ poolType: undefined, poolProgramAddress }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects invalid remote pool addresses', async () => { + for (const [opts, param] of [ + [{ remoteChainSelector: 1 }, 'remoteChainSelector'], + [{ remoteChainSelector: -1n }, 'remoteChainSelector'], + [{ remoteChainSelector: 1n << 64n }, 'remoteChainSelector'], + [{ remotePoolAddresses: [] }, 'remotePoolAddresses'], + [{ remotePoolAddresses: [''] }, 'remotePoolAddresses[0]'], + [{ remotePoolAddresses: ['0x123'] }, 'remotePoolAddresses[0]'], + [{ remotePoolAddresses: ['0xaabbccdd', 'aabbccdd'] }, 'remotePoolAddresses[1]'], + [{ remotePoolAddresses: '0x12' }, 'remotePoolAddresses'], + ] as const) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).appendRemotePoolAddresses({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + remoteChainSelector: SELECTOR, + remotePoolAddresses: REMOTE_POOLS, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed appending', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).appendRemotePoolAddresses({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + authority: AUTHORITY, + remoteChainSelector: SELECTOR, + remotePoolAddresses: REMOTE_POOLS, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendRemotePoolAddresses' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/append-remote-pool-addresses.ts b/ccip-sdk/src/cct/solana/token-pool/operations/append-remote-pool-addresses.ts new file mode 100644 index 00000000..455a1536 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/append-remote-pool-addresses.ts @@ -0,0 +1,174 @@ +import type { Buffer } from 'buffer' + +import { type PublicKey, SystemProgram } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type PoolProgramRef, + createTokenPoolProgram, + deriveTokenPoolChainConfigPda, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + U64_MAX, + parseNonEmptyHexBytes, + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, + validateBigInt, +} from '../../validate.ts' + +/** Parameters shared by Solana remote pool address appending generation and execution. */ +type AppendRemotePoolAddressesParams = PoolProgramRef & { + /** Token mint address managed by the local pool. */ + tokenAddress: string + /** CCIP selector of the remote chain (`u64`). */ + remoteChainSelector: bigint + /** + * Non-empty array of non-empty hex-encoded remote pool addresses, optionally `0x`-prefixed. + * Stored at native byte length; unlike `remoteTokenAddress`, not left-padded to 32 bytes. + */ + remotePoolAddresses: string[] + /** Pool owner. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedAppendRemotePoolAddressesParams = { + tokenAddress: PublicKey + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey + remoteChainSelector: bigint + remotePoolAddresses: Buffer[] +} + +/** Parameters for unsigned Solana remote pool address appending. */ +export type GenerateAppendRemotePoolAddressesParams = + SolanaGenerateParams + +/** Unsigned Solana remote pool address appending result. */ +export type GenerateAppendRemotePoolAddressesResult = UnsignedSolanaTx + +/** Parameters for executing Solana remote pool address appending. */ +export type ExecuteAppendRemotePoolAddressesParams = + SolanaExecuteParams + +/** Result of executing Solana remote pool address appending. */ +export type ExecuteAppendRemotePoolAddressesResult = TransactionResult + +/** + * Appends remote pool addresses to an initialized remote-chain config. + * + * @remarks Existing addresses are retained. The remote-chain config must already exist. The pool + * rejects addresses already present; duplicate addresses in this request are rejected. To clear + * all pools, use `editChainRemoteConfig` with `remotePoolAddresses: []`. + */ +export class AppendRemotePoolAddresses extends SolanaOperation< + AppendRemotePoolAddressesParams, + UnsignedSolanaTx, + ParsedAppendRemotePoolAddressesParams +> { + readonly name = 'appendRemotePoolAddresses' + + /** Parses addresses and defaults authority to payer without mutating caller params. */ + protected override parse( + params: GenerateAppendRemotePoolAddressesParams, + ): ParsedAppendRemotePoolAddressesParams { + validateBigInt(this.name, 'remoteChainSelector', params.remoteChainSelector, 0n, U64_MAX) + + if (!Array.isArray(params.remotePoolAddresses) || params.remotePoolAddresses.length === 0) { + throw new CCTParamsInvalidError(this.name, 'remotePoolAddresses', 'must be a non-empty array') + } + + const remotePoolAddresses = params.remotePoolAddresses.map((address, i) => + parseNonEmptyHexBytes(this.name, `remotePoolAddresses[${i}]`, address), + ) + const seen = new Set() + + for (const [i, address] of remotePoolAddresses.entries()) { + const hex = address.toString('hex') + if (seen.has(hex)) { + throw new CCTParamsInvalidError( + this.name, + `remotePoolAddresses[${i}]`, + 'must not duplicate a remote pool address', + ) + } + seen.add(hex) + } + + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram: resolvePoolProgram(this.name, params), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + remoteChainSelector: params.remoteChainSelector, + remotePoolAddresses, + } + } + + /** Builds the unsigned Solana `appendRemotePoolAddresses` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedAppendRemotePoolAddressesParams, + ): Promise { + const program = createTokenPoolProgram(chain, opts.poolProgram, opts.payer) + const instruction = await program.methods + .appendRemotePoolAddresses( + new BN(opts.remoteChainSelector.toString()), + opts.tokenAddress, + opts.remotePoolAddresses.map((address) => ({ address })), + ) + .accountsStrict({ + state: deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress), + chainConfig: deriveTokenPoolChainConfigPda( + opts.poolProgram, + opts.remoteChainSelector, + opts.tokenAddress, + ), + authority: opts.authority, + systemProgram: SystemProgram.programId, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}, remoteChainSelector = ${opts.remoteChainSelector}`, + ) + + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the pool owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteAppendRemotePoolAddressesParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'appendRemotePoolAddresses requires authority to be the executing wallet. Use generateUnsignedAppendRemotePoolAddresses for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/apply-chain-updates.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/apply-chain-updates.test.ts new file mode 100644 index 00000000..2c27bb23 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/apply-chain-updates.test.ts @@ -0,0 +1,424 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { CCIPError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { resolveTokenPoolProgram } from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const SELECTOR = 5009297550715157269n +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function chain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(), + connection: { + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + } as unknown as SolanaChain +} + +function batchChains() { + return [3n, 4n, 5n].map((remoteChainSelector, i) => ({ + remoteChainSelector, + remoteTokenAddress: `0x${(i + 1).toString(16).padStart(40, '0')}`, + remotePoolAddresses: [`0x${(i + 11).toString(16).padStart(40, '0')}`], + remoteTokenDecimals: 6 + i, + inboundRateLimiterConfig: { enabled: false as const }, + outboundRateLimiterConfig: { enabled: true as const, capacity: 100n, rate: 10n }, + })) +} + +function generateBatches(opts = {}) { + return SolanaTokenManager.fromChain(chain()).generateUnsignedApplyChainUpdates({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + remoteChainSelectorsToRemove: [SELECTOR], + chainsToAdd: [ + { + remoteChainSelector: SELECTOR, + remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + remotePoolAddresses: ['0x1234567890abcdef1234567890abcdef12345678'], + remoteTokenDecimals: 18, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: true, capacity: 100n, rate: 10n }, + }, + ], + ...opts, + }) +} + +async function generate(opts = {}) { + const [unsigned] = await generateBatches(opts) + return unsigned! +} + +describe('ApplyChainUpdates (cct/solana)', () => { + describe('generate', () => { + it('builds delete, initialize, edit, and rate-limit instructions', async () => { + const unsigned = await generate() + const poolProgram = resolveTokenPoolProgram('burn-mint') + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.ok(unsigned.instructions.every(({ programId }) => programId.equals(poolProgram))) + assert.deepEqual( + unsigned.instructions.map( + (instruction) => tokenPoolCoder.instruction.decode(instruction.data)!.name, + ), + [ + 'deleteChainConfig', + 'initChainRemoteConfig', + 'editChainRemoteConfig', + 'setChainRateLimit', + ], + ) + }) + + it('builds delete, then per-chain init, edit, and rate-limit instructions for multiple chains', async () => { + const batches = await generateBatches({ + remoteChainSelectorsToRemove: [1n, 2n], + chainsToAdd: [ + { + remoteChainSelector: 3n, + remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + remotePoolAddresses: ['0x1234567890abcdef1234567890abcdef12345678'], + remoteTokenDecimals: 18, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: true, capacity: 100n, rate: 10n }, + }, + { + remoteChainSelector: 4n, + remoteTokenAddress: '0xaabbccddeeff00112233445566778899aabbccdd', + remotePoolAddresses: [], + remoteTokenDecimals: 6, + inboundRateLimiterConfig: { enabled: true, capacity: 200n, rate: 20n }, + outboundRateLimiterConfig: { enabled: false }, + }, + { + remoteChainSelector: 5n, + remoteTokenAddress: '0x11223344556677889900aabbccddeeff00112233', + remotePoolAddresses: ['0x1234', '0xabcd'], + remoteTokenDecimals: 8, + inboundRateLimiterConfig: { enabled: true, capacity: 5_000n, rate: 50n }, + outboundRateLimiterConfig: { enabled: false }, + }, + ], + }) + + assert.deepEqual( + batches.flatMap((batch) => + batch.instructions.map( + (instruction) => tokenPoolCoder.instruction.decode(instruction.data)!.name, + ), + ), + [ + 'deleteChainConfig', + 'deleteChainConfig', + 'initChainRemoteConfig', + 'editChainRemoteConfig', + 'setChainRateLimit', + 'initChainRemoteConfig', + 'editChainRemoteConfig', + 'setChainRateLimit', + 'initChainRemoteConfig', + 'editChainRemoteConfig', + 'setChainRateLimit', + ], + ) + }) + + it('packs large updates without splitting a chain instruction group', async () => { + const batches = await SolanaTokenManager.fromChain(chain()).generateUnsignedApplyChainUpdates( + { + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + remoteChainSelectorsToRemove: [], + chainsToAdd: batchChains(), + }, + ) + + assert.equal(batches.length, 2) + assert.deepEqual( + batches.flatMap((batch) => + batch.instructions.map( + (instruction) => tokenPoolCoder.instruction.decode(instruction.data)!.name, + ), + ), + [ + 'initChainRemoteConfig', + 'editChainRemoteConfig', + 'setChainRateLimit', + 'initChainRemoteConfig', + 'editChainRemoteConfig', + 'setChainRateLimit', + 'initChainRemoteConfig', + 'editChainRemoteConfig', + 'setChainRateLimit', + ], + ) + assert.ok(batches.every((batch) => batch.instructions.length % 3 === 0)) + }) + + it('rejects a chain update that cannot fit one transaction', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).generateUnsignedApplyChainUpdates({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + remoteChainSelectorsToRemove: [], + chainsToAdd: [ + { + ...batchChains()[0]!, + remotePoolAddresses: Array.from( + { length: 30 }, + (_, i) => `0x${(i + 1).toString(16).padStart(40, '0')}`, + ), + }, + ], + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'chainsToAdd' && + err.message.includes('chain selector 0x3 (30 remote pool addresses)'), + ) + }) + + it('sets disabled rate-limit configs like EVM applyChainUpdates', async () => { + const unsigned = await generate({ + remoteChainSelectorsToRemove: [], + chainsToAdd: [ + { + remoteChainSelector: SELECTOR, + remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + remotePoolAddresses: ['0x1234567890abcdef1234567890abcdef12345678'], + remoteTokenDecimals: 18, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: false }, + }, + ], + }) + + const decoded = tokenPoolCoder.instruction.decode(unsigned.instructions[2]!.data) + + assert.equal(unsigned.instructions.length, 3) + assert.ok(decoded) + assert.equal(decoded.name, 'setChainRateLimit') + }) + + it('uses a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await generate({ poolType: undefined, poolProgramAddress }) + + assert.ok( + unsigned.instructions.every(({ programId }) => programId.toBase58() === poolProgramAddress), + ) + }) + }) + + describe('validation', () => { + it('rejects invalid chain updates', async () => { + for (const [opts, param] of [ + [{ remoteChainSelectorsToRemove: null }, 'remoteChainSelectorsToRemove'], + [{ remoteChainSelectorsToRemove: [-1n] }, 'remoteChainSelector'], + [{ chainsToAdd: null }, 'chainsToAdd'], + [{ chainsToAdd: [], remoteChainSelectorsToRemove: [] }, 'chainsToAdd'], + [{ chainsToAdd: [null] }, 'chainsToAdd[0]'], + [ + { + chainsToAdd: [ + { + remoteChainSelector: SELECTOR, + remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + remotePoolAddresses: [], + remoteTokenDecimals: 256, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: false }, + }, + ], + }, + 'remoteTokenDecimals', + ], + [ + { + chainsToAdd: [ + { + remoteChainSelector: SELECTOR, + remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + remotePoolAddresses: null, + remoteTokenDecimals: 18, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: false }, + }, + ], + }, + 'remotePoolAddresses', + ], + [ + { + chainsToAdd: [ + { + remoteChainSelector: SELECTOR, + remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + remotePoolAddresses: ['0x'], + remoteTokenDecimals: 18, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: false }, + }, + ], + }, + 'chainsToAdd[0].remotePoolAddresses[0]', + ], + [ + { + chainsToAdd: [ + { + remoteChainSelector: SELECTOR, + remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + remotePoolAddresses: ['0x1234', '0x1234'], + remoteTokenDecimals: 18, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: false }, + }, + ], + }, + 'chainsToAdd[0].remotePoolAddresses[1]', + ], + [ + { + chainsToAdd: [ + { + remoteChainSelector: SELECTOR, + remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + remotePoolAddresses: [], + remoteTokenDecimals: 18, + inboundRateLimiterConfig: { enabled: false, capacity: 1n, rate: 0n }, + outboundRateLimiterConfig: { enabled: false }, + }, + ], + }, + 'inbound', + ], + ] as const) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + }) + + describe('execute', () => { + it('signs, submits, and returns all tx hashes', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).applyChainUpdates({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + remoteChainSelectorsToRemove: [SELECTOR], + chainsToAdd: [ + { + remoteChainSelector: SELECTOR, + remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + remotePoolAddresses: ['0x1234567890abcdef1234567890abcdef12345678'], + remoteTokenDecimals: 18, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: true, capacity: 100n, rate: 10n }, + }, + ], + wallet: WALLET, + }) + + assert.deepEqual(result, { hashes: [HASH], chainSelectors: [[`0x${SELECTOR.toString(16)}`]] }) + }) + + it('submits every safely packed batch and returns all hashes', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).applyChainUpdates({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + remoteChainSelectorsToRemove: [], + chainsToAdd: batchChains(), + wallet: WALLET, + }) + + assert.deepEqual(result, { hashes: [HASH, HASH], chainSelectors: [['0x3', '0x4'], ['0x5']] }) + }) + + it('attaches committed hashes when a later batch fails', async () => { + let simulations = 0 + const failedChain = submitChain() + failedChain.connection.simulateTransaction = (async () => { + simulations++ + return { + value: { err: simulations >= 2 ? { custom: 1 } : null, logs: [], unitsConsumed: 1 }, + } + }) as never + + await assert.rejects( + () => + SolanaTokenManager.fromChain(failedChain).applyChainUpdates({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + remoteChainSelectorsToRemove: [], + chainsToAdd: batchChains(), + wallet: WALLET, + }), + (error: unknown) => + CCIPError.isCCIPError(error) && + error.context.committedHashes instanceof Array && + error.context.committedHashes[0] === HASH && + error.context.committedChainSelectors instanceof Array && + error.context.committedChainSelectors[0]?.join() === '0x3,0x4' && + error.context.failedBatchIndex === 1 && + error.context.totalBatches === 2, + ) + }) + + it('rejects a non-wallet authority for signed configuration', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).applyChainUpdates({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + authority: AUTHORITY, + remoteChainSelectorsToRemove: [SELECTOR], + chainsToAdd: [], + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyChainUpdates' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/apply-chain-updates.ts b/ccip-sdk/src/cct/solana/token-pool/operations/apply-chain-updates.ts new file mode 100644 index 00000000..12747c3d --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/apply-chain-updates.ts @@ -0,0 +1,386 @@ +import { + type TransactionInstruction, + ComputeBudgetProgram, + PublicKey, + TransactionMessage, + VersionedTransaction, +} from '@solana/web3.js' + +import { DeleteChainRemoteConfig } from './delete-chain-remote-config.ts' +import { EditChainRemoteConfig } from './edit-chain-remote-config.ts' +import { InitChainRemoteConfig } from './init-chain-remote-config.ts' +import { type RateLimitConfig, SetChainRateLimit } from './set-chain-rate-limit.ts' +import { CCIPError, CCIPMethodUnsupportedError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import type { PoolProgramRef } from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parseNonEmptyHexBytes, + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, +} from '../../validate.ts' + +const MAX_TRANSACTION_SIZE = 1232 + +type InstructionGroup = { + instructions: TransactionInstruction[] + remoteChainSelector?: bigint + remotePoolCount?: number +} + +type PackedInstructionGroup = { + transaction: UnsignedSolanaTx + chainSelectors: string[] +} + +/** A remote-chain configuration to add, matching the EVM `ChainUpdate` fields plus Solana decimals. */ +type ChainUpdate = { + /** CCIP selector of the remote chain (`u64`). */ + remoteChainSelector: bigint + /** Hex-encoded remote token address, optionally `0x`-prefixed, up to 32 bytes. */ + remoteTokenAddress: string + /** Hex-encoded remote pool addresses, optionally `0x`-prefixed; supplied addresses are non-empty and unique. */ + remotePoolAddresses: string[] + /** Remote token decimals (`u8`), required by the Solana pool account. */ + remoteTokenDecimals: number + /** Rate limit for tokens received from the remote chain. */ + inboundRateLimiterConfig: RateLimitConfig + /** Rate limit for tokens sent to the remote chain. */ + outboundRateLimiterConfig: RateLimitConfig +} + +type ApplyChainUpdatesParams = PoolProgramRef & { + /** Token mint address managed by the local pool. */ + tokenAddress: string + /** + * Remote chain configurations to add, including their rate limits. To replace a config, include + * its selector here and in `remoteChainSelectorsToRemove`. + */ + chainsToAdd: ChainUpdate[] + /** Remote chain configurations to delete before additions are initialized. */ + remoteChainSelectorsToRemove: bigint[] + /** Pool owner. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type PoolInstructionParams = PoolProgramRef & { + tokenAddress: string + payer: string + authority: string +} + +type ParsedApplyChainUpdatesParams = ApplyChainUpdatesParams & { + payer: string + authority: string +} + +function validateRemotePoolAddresses(operation: string, updates: unknown[]): void { + for (const [i, update] of updates.entries()) { + if (typeof update !== 'object' || update === null) { + throw new CCTParamsInvalidError(operation, `chainsToAdd[${i}]`, 'must be a chain update') + } + const remotePoolAddresses = (update as { remotePoolAddresses?: unknown }).remotePoolAddresses + if (!Array.isArray(remotePoolAddresses)) continue + + const pools = new Set() + for (const [j, address] of remotePoolAddresses.entries()) { + const parsed = parseNonEmptyHexBytes( + operation, + `chainsToAdd[${i}].remotePoolAddresses[${j}]`, + address, + ) + if (pools.has(parsed.toString('hex'))) { + throw new CCTParamsInvalidError( + operation, + `chainsToAdd[${i}].remotePoolAddresses[${j}]`, + 'must not duplicate a remote pool address', + ) + } + pools.add(parsed.toString('hex')) + } + } +} + +/** Serializes a conservative v0 transaction, including compute-budget overhead, to check its size. */ +function fitsInTransaction(payer: PublicKey, instructions: TransactionInstruction[]): boolean { + try { + const transaction = new VersionedTransaction( + new TransactionMessage({ + payerKey: payer, + recentBlockhash: PublicKey.default.toBase58(), + instructions: [ + // submit may add this instruction after simulation; include it so batches remain safe. + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }), + ...instructions, + ], + }).compileToV0Message(), + ) + return transaction.serialize().length <= MAX_TRANSACTION_SIZE + } catch { + return false + } +} + +/** Packs ordered instruction groups without splitting a remote-chain update across transactions. */ +function packInstructionGroups( + operation: string, + payer: PublicKey, + groups: InstructionGroup[], +): PackedInstructionGroup[] { + const batches: PackedInstructionGroup[] = [] + let instructions: TransactionInstruction[] = [] + let chainSelectors: string[] = [] + + for (const group of groups) { + if (!fitsInTransaction(payer, group.instructions)) { + const detail = + group.remoteChainSelector === undefined + ? 'a delete' + : `chain selector 0x${group.remoteChainSelector.toString(16)} (${group.remotePoolCount} remote pool addresses)` + throw new CCTParamsInvalidError( + operation, + 'chainsToAdd', + `${detail} exceeds Solana's ${MAX_TRANSACTION_SIZE}-byte transaction limit`, + ) + } + if ( + instructions.length && + !fitsInTransaction(payer, [...instructions, ...group.instructions]) + ) { + batches.push({ + transaction: { family: ChainFamily.Solana, instructions, mainIndex: 0 }, + chainSelectors, + }) + instructions = [] + chainSelectors = [] + } + instructions.push(...group.instructions) + if (group.remoteChainSelector !== undefined) { + chainSelectors.push(`0x${group.remoteChainSelector.toString(16)}`) + } + } + + if (instructions.length) { + batches.push({ + transaction: { family: ChainFamily.Solana, instructions, mainIndex: 0 }, + chainSelectors, + }) + } + return batches +} + +/** Parameters for unsigned Solana token pool chain updates. */ +export type GenerateApplyChainUpdatesParams = SolanaGenerateParams + +/** Unsigned Solana token pool chain updates result. */ +export type GenerateApplyChainUpdatesResult = UnsignedSolanaTx[] + +/** Parameters for executing Solana token pool chain updates. */ +export type ExecuteApplyChainUpdatesParams = SolanaExecuteParams + +/** All confirmed transaction hashes for Solana token pool chain updates. */ +export type ExecuteApplyChainUpdatesResult = { hashes: string[]; chainSelectors: string[][] } + +/** + * Applies the EVM `applyChainUpdates` equivalent as Solana instructions. + * + * @remarks + * This preserves EVM ordering: all removals run first, then each added chain is initialized, + * configured with remote pools, and assigned both rate-limit configs. EVM-style replacement is + * supported by listing a selector in both `remoteChainSelectorsToRemove` and `chainsToAdd`; + * adding an existing selector without removing it fails. Updates are packed into one or more + * transactions, keeping each chain's initialization, configuration, and rate-limit instructions + * together. Batches are submitted sequentially; a later failure leaves earlier batches committed. + */ +export class ApplyChainUpdates extends SolanaOperation< + ApplyChainUpdatesParams, + UnsignedSolanaTx, + ParsedApplyChainUpdatesParams +> { + readonly name = 'applyChainUpdates' + + /** Validates the batch envelope; component operations validate each chain update. */ + protected override parse(params: GenerateApplyChainUpdatesParams): ParsedApplyChainUpdatesParams { + parsePublicKey(this.name, 'tokenAddress', params.tokenAddress) + parsePublicKey(this.name, 'payer', params.payer) + resolvePoolProgram(this.name, params) + if (!Array.isArray(params.chainsToAdd)) { + throw new CCTParamsInvalidError(this.name, 'chainsToAdd', 'must be an array') + } + if (!Array.isArray(params.remoteChainSelectorsToRemove)) { + throw new CCTParamsInvalidError(this.name, 'remoteChainSelectorsToRemove', 'must be an array') + } + if (!params.chainsToAdd.length && !params.remoteChainSelectorsToRemove.length) { + throw new CCTParamsInvalidError( + this.name, + 'chainsToAdd', + 'at least one of chainsToAdd or remoteChainSelectorsToRemove must be non-empty', + ) + } + validateRemotePoolAddresses(this.name, params.chainsToAdd) + + return { + ...params, + authority: + params.authority === undefined + ? params.payer + : parsePublicKey(this.name, 'authority', params.authority).toBase58(), + } + } + + /** Builds the initialize, edit, and rate-limit instructions for one added chain. */ + private async buildAddInstructions( + chain: SolanaChain, + pool: PoolInstructionParams, + update: ChainUpdate, + ): Promise { + const config = { + ...pool, + remoteChainSelector: update.remoteChainSelector, + remoteTokenAddress: update.remoteTokenAddress, + remotePoolAddresses: update.remotePoolAddresses, + remoteTokenDecimals: update.remoteTokenDecimals, + } + const init = await new InitChainRemoteConfig().generate(chain, config) + const edit = await new EditChainRemoteConfig().generate(chain, config) + const rateLimit = await new SetChainRateLimit().generate(chain, { + ...pool, + remoteChainSelector: update.remoteChainSelector, + inbound: update.inboundRateLimiterConfig, + outbound: update.outboundRateLimiterConfig, + }) + return [...init.instructions, ...edit.instructions, ...rateLimit.instructions] + } + + /** Builds ordered delete and per-chain update instruction groups. */ + private async buildInstructionGroups( + chain: SolanaChain, + params: ParsedApplyChainUpdatesParams, + ): Promise { + const pool: PoolInstructionParams = { + tokenAddress: params.tokenAddress, + payer: params.payer, + authority: params.authority, + ...(params.poolType === undefined + ? { poolProgramAddress: params.poolProgramAddress } + : { poolType: params.poolType }), + } + const groups: InstructionGroup[] = [] + + for (const remoteChainSelector of params.remoteChainSelectorsToRemove) { + const tx = await new DeleteChainRemoteConfig().generate(chain, { + ...pool, + remoteChainSelector, + }) + groups.push({ instructions: tx.instructions }) + } + for (const update of params.chainsToAdd) { + groups.push({ + instructions: await this.buildAddInstructions(chain, pool, update), + remoteChainSelector: update.remoteChainSelector, + remotePoolCount: update.remotePoolAddresses.length, + }) + } + return groups + } + + /** Builds all instructions in contract-equivalent order as one unsigned transaction. */ + protected async buildUnsigned( + chain: SolanaChain, + params: ParsedApplyChainUpdatesParams, + ): Promise { + return { + family: ChainFamily.Solana, + instructions: (await this.buildInstructionGroups(chain, params)).flatMap( + (group) => group.instructions, + ), + mainIndex: 0, + } + } + + /** + * Unsupported because this operation may require multiple transactions. + * @see {@link generateBatch}. + */ + override generate( + _chain: SolanaChain, + _params: GenerateApplyChainUpdatesParams, + ): Promise { + throw new CCIPMethodUnsupportedError('ApplyChainUpdates', 'generate; use generateBatch') + } + + /** Builds one or more ordered transactions without splitting a per-chain update group. */ + async generateBatch( + chain: SolanaChain, + params: GenerateApplyChainUpdatesParams, + ): Promise { + const parsed = this.prepare(params) + return packInstructionGroups( + this.name, + new PublicKey(parsed.payer), + await this.buildInstructionGroups(chain, parsed), + ).map(({ transaction }) => transaction) + } + + /** + * Unsupported because this operation may require multiple transactions. + * @see {@link executeBatch}. + */ + override execute( + _chain: SolanaChain, + _params: ExecuteApplyChainUpdatesParams, + ): Promise { + throw new CCIPMethodUnsupportedError('ApplyChainUpdates', 'execute; use executeBatch') + } + + /** Signs, submits, and confirms each packed transaction, returning every transaction hash. */ + async executeBatch( + chain: SolanaChain, + params: ExecuteApplyChainUpdatesParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + validateAuthorityMatchesWallet( + this.name, + new PublicKey(parsed.authority), + wallet.publicKey, + 'applyChainUpdates requires authority to be the executing wallet. Use generateUnsignedApplyChainUpdates for externally signed transactions.', + ) + + const batches = packInstructionGroups( + this.name, + wallet.publicKey, + await this.buildInstructionGroups(chain, parsed), + ) + const hashes: string[] = [] + const chainSelectors: string[][] = [] + + for (const [failedBatchIndex, batch] of batches.entries()) { + try { + hashes.push((await submit(chain, wallet, batch.transaction, this.name, computeUnits)).hash) + chainSelectors.push(batch.chainSelectors) + } catch (error) { + if (CCIPError.isCCIPError(error)) { + Object.assign(error.context, { + committedHashes: hashes, + committedChainSelectors: chainSelectors, + failedBatchIndex, + totalBatches: batches.length, + }) + } + throw error + } + } + + return { hashes, chainSelectors } + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.test.ts new file mode 100644 index 00000000..280b4ba0 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.test.ts @@ -0,0 +1,211 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveTokenPoolConfigPda, resolveTokenPoolProgram } from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const ALLOWED = Keypair.generate().publicKey.toBase58() +const SECOND_ALLOWED = Keypair.generate().publicKey.toBase58() +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...stubChain(), + connection: { + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedConfigureAllowlist({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + add: [ALLOWED], + enabled: true, + ...opts, + }) +} + +describe('ConfigureAllowlist (cct/solana)', () => { + describe('generate', () => { + it('builds an unsigned configure allowlist instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('burn-mint') + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction.programId.toBase58(), poolProgram.toBase58()) + assert.deepEqual( + instruction.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: TOKEN, isSigner: false, isWritable: false }, + { pubkey: AUTHORITY, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId.toBase58(), isSigner: false, isWritable: false }, + ], + ) + }) + + it('encodes multiple addresses and overwrites enforcement', async () => { + const unsigned = await generate({ add: [ALLOWED, SECOND_ALLOWED], enabled: false }) + const decoded = tokenPoolCoder.instruction.decode(unsigned.instructions[0]!.data) + + assert.ok(decoded) + assert.equal(decoded.name, 'configureAllowList') + assert.deepEqual( + (decoded.data as { add: PublicKey[] }).add.map((address) => address.toBase58()), + [ALLOWED, SECOND_ALLOWED], + ) + assert.equal((decoded.data as { enabled: boolean }).enabled, false) + }) + + it('encodes a toggle without addresses', async () => { + const unsigned = await generate({ add: [], enabled: false }) + const decoded = tokenPoolCoder.instruction.decode(unsigned.instructions[0]!.data) + + assert.ok(decoded) + assert.equal(decoded.name, 'configureAllowList') + assert.deepEqual((decoded.data as { add: PublicKey[] }).add, []) + assert.equal((decoded.data as { enabled: boolean }).enabled, false) + }) + + it('uses a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await SolanaTokenManager.fromChain( + stubChain(), + ).generateUnsignedConfigureAllowlist({ + tokenAddress: TOKEN, + poolProgramAddress, + payer: PAYER, + authority: AUTHORITY, + add: [ALLOWED], + enabled: true, + }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects invalid pool program references', async () => { + await assert.rejects( + () => generate({ poolType: 'custom' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'configureAllowlist' && + err.context.param === 'poolType', + ) + }) + + it('rejects non-array addresses to add', async () => { + await assert.rejects( + () => generate({ add: 'not-an-array' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'configureAllowlist' && + err.context.param === 'add', + ) + }) + + it('rejects invalid addresses to add', async () => { + await assert.rejects( + () => generate({ add: ['not-a-pubkey'] }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'configureAllowlist' && + err.context.param === 'add[0]', + ) + }) + + it('rejects duplicate addresses to add', async () => { + await assert.rejects( + () => generate({ add: [ALLOWED, ALLOWED] }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'configureAllowlist' && + err.context.param === 'add', + ) + }) + + it('rejects non-boolean enabled values', async () => { + await assert.rejects( + () => generate({ enabled: 'true' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'configureAllowlist' && + err.context.param === 'enabled', + ) + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).configureAllowlist({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + add: [ALLOWED], + enabled: true, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed configuration', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).configureAllowlist({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + authority: AUTHORITY, + add: [ALLOWED], + enabled: true, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'configureAllowlist' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.ts b/ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.ts new file mode 100644 index 00000000..fba26abf --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.ts @@ -0,0 +1,148 @@ +import { type PublicKey, SystemProgram } from '@solana/web3.js' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { type UnsignedSolanaTx, isWallet } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type PoolProgramRef, + createTokenPoolProgram, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, +} from '../../validate.ts' + +/** Parameters shared by Solana token pool `configureAllowlist` generation and execution. */ +type ConfigureAllowlistParams = PoolProgramRef & { + /** Token mint address managed by the pool. */ + tokenAddress: string + /** Addresses to append to the pool allowlist. Must not contain duplicates. */ + add: string[] + /** Whether the pool should enforce its allowlist. */ + enabled: boolean + /** Pool owner. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedConfigureAllowlistParams = { + tokenAddress: PublicKey + poolProgram: PublicKey + add: PublicKey[] + enabled: boolean + payer: PublicKey + authority: PublicKey +} + +/** Parameters for unsigned Solana token pool allowlist configuration. */ +export type GenerateConfigureAllowlistParams = SolanaGenerateParams + +/** Unsigned Solana token pool allowlist configuration result. */ +export type GenerateConfigureAllowlistResult = UnsignedSolanaTx + +/** Parameters for executing Solana token pool allowlist configuration. */ +export type ExecuteConfigureAllowlistParams = SolanaExecuteParams + +/** Result of executing Solana token pool allowlist configuration. */ +export type ExecuteConfigureAllowlistResult = TransactionResult + +/** Adds addresses to and enables or disables a Solana token pool allowlist. */ +export class ConfigureAllowlist extends SolanaOperation< + ConfigureAllowlistParams, + UnsignedSolanaTx, + ParsedConfigureAllowlistParams +> { + readonly name = 'configureAllowlist' + + /** Parses public keys and defaults authority to payer without mutating caller params. */ + protected override parse( + params: GenerateConfigureAllowlistParams, + ): ParsedConfigureAllowlistParams { + if (!Array.isArray(params.add)) { + throw new CCTParamsInvalidError(this.name, 'add', 'must be an array') + } + if (typeof params.enabled !== 'boolean') { + throw new CCTParamsInvalidError(this.name, 'enabled', 'must be a boolean') + } + + const add = params.add.map((address, index) => + parsePublicKey(this.name, `add[${index}]`, address), + ) + if (new Set(add.map((address) => address.toBase58())).size !== add.length) { + throw new CCTParamsInvalidError(this.name, 'add', 'must not contain duplicate addresses') + } + + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram: resolvePoolProgram(this.name, params), + add, + enabled: params.enabled, + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + } + } + + /** Builds the unsigned Solana `configureAllowList` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedConfigureAllowlistParams, + ): Promise { + const program = createTokenPoolProgram(chain, opts.poolProgram, opts.payer) + const state = deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress) + + const instruction = await program.methods + .configureAllowList(opts.add, opts.enabled) + .accountsStrict({ + state, + mint: opts.tokenAddress, + authority: opts.authority, + systemProgram: SystemProgram.programId, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the pool owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteConfigureAllowlistParams, + ): Promise { + const { wallet, computeUnits, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const generateParams: GenerateConfigureAllowlistParams = { + ...rest, + payer: wallet.publicKey.toBase58(), + } + const parsed = this.prepare(generateParams) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'configureAllowlist requires authority to be the executing wallet. Use generateUnsignedConfigureAllowlist for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/create-token-multisig.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/create-token-multisig.test.ts new file mode 100644 index 00000000..02c53625 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/create-token-multisig.test.ts @@ -0,0 +1,167 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { MINT_SIZE, MULTISIG_SIZE, MintLayout, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveTokenPoolSignerPda } from '../../programs/token-pool.ts' + +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const MINT = Keypair.generate().publicKey.toBase58() +const POOL_PROGRAM = '41FGToCmdaWa1dgZLKFAjvmx6e6AjVTX7SVRibvsMGVB' +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function mintData(mintAuthority: PublicKey | null = new PublicKey(AUTHORITY)) { + const data = Buffer.alloc(MINT_SIZE) + MintLayout.encode( + { + mintAuthorityOption: mintAuthority ? 1 : 0, + mintAuthority: mintAuthority ?? PublicKey.default, + supply: 0n, + decimals: 0, + isInitialized: true, + freezeAuthorityOption: 0, + freezeAuthority: PublicKey.default, + }, + data, + ) + return data +} + +function stubChain(mintAuthority?: PublicKey | null): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async () => ({ + owner: TOKEN_PROGRAM_ID, + data: mintData(mintAuthority), + executable: false, + lamports: 1, + }), + getMinimumBalanceForRentExemption: async (space: number) => { + assert.equal(space, MULTISIG_SIZE) + return 123 + }, + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedCreateTokenMultisig({ + tokenAddress: MINT, + poolType: 'burn-mint', + threshold: 2, + payer: PAYER, + seed: 'seed', + ...opts, + }) +} + +describe('CreateTokenMultisig (cct/solana)', () => { + describe('generate', () => { + it('builds a pool-autonomous threshold-two multisig', async () => { + const unsigned = await generate({ + threshold: 2, + additionalSigners: [Keypair.generate().publicKey.toBase58()], + }) + const [createIx, initIx] = unsigned.instructions + assert.ok(createIx) + assert.ok(initIx) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.match(unsigned.multisigAddress, /^[1-9A-HJ-NP-Za-km-z]+$/) + assert.equal(createIx.programId.toBase58(), SystemProgram.programId.toBase58()) + assert.equal(initIx.programId.toBase58(), TOKEN_PROGRAM_ID.toBase58()) + assert.equal(initIx.data[0], 2) // InitializeMultisig + assert.equal(initIx.data[1], 2) // threshold + + const poolSigner = deriveTokenPoolSignerPda(new PublicKey(POOL_PROGRAM), new PublicKey(MINT)) + assert.equal(initIx.keys.filter((key) => key.pubkey.equals(poolSigner)).length, 2) + assert.ok(initIx.keys.some((key) => key.pubkey.equals(new PublicKey(AUTHORITY)))) + assert.ok(!initIx.keys.some((key) => key.pubkey.equals(new PublicKey(PAYER)))) + }) + + it('builds the canonical threshold-one pool multisig', async () => { + const unsigned = await generate({ threshold: 1 }) + const poolSigner = deriveTokenPoolSignerPda(new PublicKey(POOL_PROGRAM), new PublicKey(MINT)) + + assert.equal(unsigned.instructions[1]!.data[1], 1) + assert.equal( + unsigned.instructions[1]!.keys.filter((key) => key.pubkey.equals(poolSigner)).length, + 1, + ) + }) + + it('adds additional signers', async () => { + const signer = Keypair.generate().publicKey + const unsigned = await generate({ additionalSigners: [signer.toBase58()] }) + + assert.ok(unsigned.instructions[1]!.keys.some((key) => key.pubkey.equals(signer))) + }) + }) + + describe('validation', () => { + it('rejects invalid pool type', async () => { + await assert.rejects( + () => generate({ poolType: 'custom' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'createTokenMultisig' && + err.context.param === 'poolType', + ) + }) + + it('requires an independent governance quorum', async () => { + await assert.rejects( + () => generate(), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'createTokenMultisig' && + err.context.param === 'threshold', + ) + }) + + it('rejects mint without mint authority', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain(null)).generateUnsignedCreateTokenMultisig({ + tokenAddress: MINT, + poolType: 'burn-mint', + threshold: 2, + payer: PAYER, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'createTokenMultisig' && + err.context.param === 'tokenAddress', + ) + }) + }) + + describe('execute', () => { + it('rejects signed execute when wallet is not mint authority', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).createTokenMultisig({ + tokenAddress: MINT, + poolType: 'burn-mint', + threshold: 2, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'createTokenMultisig' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/create-token-multisig.ts b/ccip-sdk/src/cct/solana/token-pool/operations/create-token-multisig.ts new file mode 100644 index 00000000..8eccfb7d --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/create-token-multisig.ts @@ -0,0 +1,244 @@ +import { MULTISIG_SIZE, createInitializeMultisigInstruction, unpackMint } from '@solana/spl-token' +import { PublicKey, SystemProgram } from '@solana/web3.js' +import { concat, hexlify, randomBytes, sha256, toUtf8Bytes } from 'ethers' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { resolveTokenMint } from '../../../../solana/utils.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type TokenPoolType, + deriveTokenPoolSignerPda, + resolveTokenPoolProgram, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parsePublicKey, + validateAuthorityMatchesWallet, + validateInteger, + validateNonEmptyString, + validatePoolType, +} from '../../validate.ts' + +export const SOLANA_MULTISIG_MAX_SIGNERS = 11 + +type MintAccount = NonNullable[1]> + +/** + * Parameters for creating an SPL Token multisig account for a Solana SPL mint. + * + * The pool signer PDA occupies `threshold` slots so it can mint autonomously. The mint authority + * read from `tokenAddress` and `additionalSigners` supply the independent signer slots. + */ +type CreateTokenMultisigParams = { + tokenAddress: string + poolType: TokenPoolType + threshold: number + /** Extra multisig member addresses in addition to pool signer PDA and mint authority. */ + additionalSigners?: string[] + /** Optional human seed; internally hashed with mint to fit Solana's 32-byte seed limit. */ + seed?: string +} + +/** Parameters for unsigned Solana token multisig generation. */ +export type GenerateCreateTokenMultisigParams = SolanaGenerateParams + +type ParsedCreateTokenMultisigParams = { + tokenMint: PublicKey + poolProgram: PublicKey + payer: PublicKey + threshold: number + additionalSigners: PublicKey[] + seed?: string +} + +/** Unsigned token multisig transaction plus the created multisig address. */ +export type GenerateCreateTokenMultisigResult = UnsignedSolanaTx & { multisigAddress: string } + +/** Parameters for executing Solana token multisig creation. */ +export type ExecuteCreateTokenMultisigParams = SolanaExecuteParams + +/** Result of executing Solana token multisig creation. */ +export type ExecuteCreateTokenMultisigResult = TransactionResult & { multisigAddress: string } + +function dedupePublicKeys(signers: PublicKey[]) { + const seen = new Set() + return signers.filter((signer) => { + const address = signer.toBase58() + if (seen.has(address)) return false + seen.add(address) + return true + }) +} + +function validatePoolMultisigConfig( + operation: string, + signers: PublicKey[], + poolSigner: PublicKey, + threshold: number, +) { + const poolSignerCount = signers.filter((signer) => signer.equals(poolSigner)).length + const nonPoolSignerCount = signers.length - poolSignerCount + + if (signers.length < 2 || signers.length > SOLANA_MULTISIG_MAX_SIGNERS) { + throw new CCTParamsInvalidError( + operation, + 'additionalSigners', + `multisig must have between 2 and ${SOLANA_MULTISIG_MAX_SIGNERS} total signers`, + ) + } + if (threshold < 1) { + throw new CCTParamsInvalidError(operation, 'threshold', 'must be at least 1') + } + if (threshold > signers.length) { + throw new CCTParamsInvalidError(operation, 'threshold', 'cannot exceed total signer count') + } + if (poolSignerCount < threshold) { + throw new CCTParamsInvalidError( + operation, + 'threshold', + 'pool signer must occupy at least threshold signer slots', + ) + } + if (nonPoolSignerCount < threshold) { + throw new CCTParamsInvalidError( + operation, + 'threshold', + 'requires at least threshold non-pool signers', + ) + } +} + +function getMintAuthority( + operation: string, + tokenMint: PublicKey, + mintAccount: MintAccount, + tokenProgram: PublicKey, +): PublicKey { + const { mintAuthority } = unpackMint(tokenMint, mintAccount, tokenProgram) + if (!mintAuthority) { + throw new CCTParamsInvalidError(operation, 'tokenAddress', 'mint has no mint authority') + } + return new PublicKey(mintAuthority.toBase58()) +} + +/** + * Creates an SPL Token multisig with threshold pool signer slots and independent signers. + * + * The multisig account is derived with `createAccountWithSeed`, so no new signer keypair is needed. + */ +export class CreateTokenMultisig extends SolanaOperation< + CreateTokenMultisigParams, + GenerateCreateTokenMultisigResult, + ParsedCreateTokenMultisigParams +> { + readonly name = 'createTokenMultisig' + + /** Parses public keys, threshold, and optional seed before mint/account RPCs. */ + protected override parse( + params: GenerateCreateTokenMultisigParams, + ): ParsedCreateTokenMultisigParams { + validatePoolType(this.name, 'poolType', params.poolType) + if (params.additionalSigners !== undefined && !Array.isArray(params.additionalSigners)) { + throw new CCTParamsInvalidError(this.name, 'additionalSigners', 'must be an array') + } + validateInteger(this.name, 'threshold', params.threshold, 1, SOLANA_MULTISIG_MAX_SIGNERS) + if (params.seed !== undefined) validateNonEmptyString(this.name, 'seed', params.seed) + + return { + tokenMint: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram: resolveTokenPoolProgram(params.poolType), + payer: parsePublicKey(this.name, 'payer', params.payer), + threshold: params.threshold, + additionalSigners: (params.additionalSigners ?? []).map((signer, i) => + parsePublicKey(this.name, `additionalSigners[${i}]`, signer), + ), + ...(params.seed !== undefined && { seed: params.seed }), + } + } + + /** Builds create-with-seed and initialize-multisig instructions. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedCreateTokenMultisigParams, + mintContext?: { account: MintAccount; authority: PublicKey }, + ): Promise { + const { payer, tokenMint, poolProgram } = opts + const mintAccount = + mintContext?.account ?? (await resolveTokenMint(chain.connection, tokenMint)) + + const tokenProgram = mintAccount.owner + const authority = + mintContext?.authority ?? getMintAuthority(this.name, tokenMint, mintAccount, tokenProgram) + + const poolSigner = deriveTokenPoolSignerPda(poolProgram, tokenMint) + const nonPoolSigners = dedupePublicKeys([authority, ...opts.additionalSigners]).filter( + (signer) => !signer.equals(poolSigner), + ) + + const signers = [...Array.from({ length: opts.threshold }, () => poolSigner), ...nonPoolSigners] + validatePoolMultisigConfig(this.name, signers, poolSigner, opts.threshold) + + const seedMaterial = opts.seed ?? hexlify(randomBytes(16)).slice(2) + const seedInput = concat([toUtf8Bytes(seedMaterial), tokenMint.toBuffer()]) + const seedHash = sha256(seedInput) + const seed = seedHash.slice(2, 34) + + const multisig = await PublicKey.createWithSeed(authority, seed, tokenProgram) + const lamports = await chain.connection.getMinimumBalanceForRentExemption(MULTISIG_SIZE) + const createIx = SystemProgram.createAccountWithSeed({ + fromPubkey: payer, + newAccountPubkey: multisig, + basePubkey: authority, + seed, + space: MULTISIG_SIZE, + lamports, + programId: tokenProgram, + }) + const initIx = createInitializeMultisigInstruction( + multisig, + signers, + opts.threshold, + tokenProgram, + ) + + return { + family: ChainFamily.Solana, + instructions: [createIx, initIx], + mainIndex: 0, + multisigAddress: multisig.toBase58(), + } + } + + /** Generate, sign, simulate, send, and return the created multisig address. */ + override async execute( + chain: SolanaChain, + params: ExecuteCreateTokenMultisigParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + const mintAccount = await resolveTokenMint(chain.connection, parsed.tokenMint) + + const tokenProgram = mintAccount.owner + const mintAuthority = getMintAuthority(this.name, parsed.tokenMint, mintAccount, tokenProgram) + validateAuthorityMatchesWallet( + this.name, + mintAuthority, + wallet.publicKey, + 'createTokenMultisig requires the executing wallet to be the mint authority. Use generateUnsignedCreateTokenMultisig for vault-owned mints and have the vault sign/execute it.', + ) + + const tx = await this.buildUnsigned(chain, parsed, { + account: mintAccount, + authority: mintAuthority, + }) + const hash = await submit(chain, wallet, tx, this.name, computeUnits) + return { ...hash, multisigAddress: tx.multisigAddress } + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/delete-chain-remote-config.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/delete-chain-remote-config.test.ts new file mode 100644 index 00000000..67f13e04 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/delete-chain-remote-config.test.ts @@ -0,0 +1,158 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { + deriveTokenPoolChainConfigPda, + deriveTokenPoolConfigPda, + resolveTokenPoolProgram, +} from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const SELECTOR = 5009297550715157269n +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function chain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(), + connection: { + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(chain()).generateUnsignedDeleteChainRemoteConfig({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + remoteChainSelector: SELECTOR, + ...opts, + }) +} + +describe('DeleteChainRemoteConfig (cct/solana)', () => { + describe('generate', () => { + it('builds the delete-chain-remote-config instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('burn-mint') + const decoded = tokenPoolCoder.instruction.decode(instruction!.data) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction!.programId.toBase58(), poolProgram.toBase58()) + assert.deepEqual( + instruction!.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)).toBase58(), + isSigner: false, + isWritable: false, + }, + { + pubkey: deriveTokenPoolChainConfigPda( + poolProgram, + SELECTOR, + new PublicKey(TOKEN), + ).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: AUTHORITY, isSigner: true, isWritable: true }, + ], + ) + assert.ok(decoded) + assert.equal(decoded.name, 'deleteChainConfig') + const data = decoded.data as { + remoteChainSelector: { toString(): string } + mint: PublicKey + } + assert.equal(data.remoteChainSelector.toString(), SELECTOR.toString()) + assert.equal(data.mint.toBase58(), TOKEN) + }) + + it('supports a zero remote-chain selector', async () => { + await assert.doesNotReject(() => generate({ remoteChainSelector: 0n })) + }) + + it('uses a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await generate({ poolType: undefined, poolProgramAddress }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects invalid remote-chain selectors', async () => { + for (const remoteChainSelector of [1, -1n, 1n << 64n] as const) { + await assert.rejects( + () => generate({ remoteChainSelector }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'remoteChainSelector', + ) + } + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).deleteChainRemoteConfig({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + remoteChainSelector: SELECTOR, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed deletion', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).deleteChainRemoteConfig({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + authority: AUTHORITY, + remoteChainSelector: SELECTOR, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'deleteChainRemoteConfig' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/delete-chain-remote-config.ts b/ccip-sdk/src/cct/solana/token-pool/operations/delete-chain-remote-config.ts new file mode 100644 index 00000000..85fa816b --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/delete-chain-remote-config.ts @@ -0,0 +1,129 @@ +import type { PublicKey } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type PoolProgramRef, + createTokenPoolProgram, + deriveTokenPoolChainConfigPda, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + U64_MAX, + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, + validateBigInt, +} from '../../validate.ts' + +type DeleteChainRemoteConfigParams = PoolProgramRef & { + /** Token mint address managed by the local pool. */ + tokenAddress: string + /** CCIP selector of the remote chain (`u64`). */ + remoteChainSelector: bigint + /** Pool owner. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedDeleteChainRemoteConfigParams = { + tokenAddress: PublicKey + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey + remoteChainSelector: bigint +} + +/** Parameters for unsigned Solana token pool remote configuration deletion. */ +export type GenerateDeleteChainRemoteConfigParams = + SolanaGenerateParams + +/** Unsigned Solana token pool remote configuration deletion result. */ +export type GenerateDeleteChainRemoteConfigResult = UnsignedSolanaTx + +/** Parameters for executing Solana token pool remote configuration deletion. */ +export type ExecuteDeleteChainRemoteConfigParams = + SolanaExecuteParams + +/** Result of executing Solana token pool remote configuration deletion. */ +export type ExecuteDeleteChainRemoteConfigResult = TransactionResult + +/** Deletes an initialized remote-chain config. */ +export class DeleteChainRemoteConfig extends SolanaOperation< + DeleteChainRemoteConfigParams, + UnsignedSolanaTx, + ParsedDeleteChainRemoteConfigParams +> { + readonly name = 'deleteChainRemoteConfig' + + /** Parses addresses and defaults authority to payer without mutating caller params. */ + protected override parse( + params: GenerateDeleteChainRemoteConfigParams, + ): ParsedDeleteChainRemoteConfigParams { + validateBigInt(this.name, 'remoteChainSelector', params.remoteChainSelector, 0n, U64_MAX) + + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram: resolvePoolProgram(this.name, params), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + remoteChainSelector: params.remoteChainSelector, + } + } + + /** Builds the unsigned Solana `deleteChainConfig` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedDeleteChainRemoteConfigParams, + ): Promise { + const program = createTokenPoolProgram(chain, opts.poolProgram, opts.payer) + const instruction = await program.methods + .deleteChainConfig(new BN(opts.remoteChainSelector.toString()), opts.tokenAddress) + .accountsStrict({ + state: deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress), + chainConfig: deriveTokenPoolChainConfigPda( + opts.poolProgram, + opts.remoteChainSelector, + opts.tokenAddress, + ), + authority: opts.authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}, remoteChainSelector = ${opts.remoteChainSelector}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the pool owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteDeleteChainRemoteConfigParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'deleteChainRemoteConfig requires authority to be the executing wallet. Use generateUnsignedDeleteChainRemoteConfig for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.test.ts new file mode 100644 index 00000000..2c970ae3 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.test.ts @@ -0,0 +1,139 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { + SolanaTokenManager, + deriveTokenPoolSignerPda, + resolveTokenPoolProgram, +} from '../../index.ts' +import { deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const BURN_MINT_POOL_PROGRAM = '41FGToCmdaWa1dgZLKFAjvmx6e6AjVTX7SVRibvsMGVB' +const LOCK_RELEASE_POOL_PROGRAM = '8eqh8wppT9c5rw4ERqNCffvU6cNFJWff9WmkcYtmGiqC' +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedDeployTokenPool({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + ...opts, + }) +} + +describe('DeployTokenPool (cct/solana)', () => { + describe('generate', () => { + it('builds unsigned initialize pool instruction', async () => { + const unsigned = await generate() + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + assert.equal(unsigned.instructions[0]!.programId.toBase58(), BURN_MINT_POOL_PROGRAM) + assert.equal( + unsigned.poolAddress, + deriveTokenPoolConfigPda( + new PublicKey(BURN_MINT_POOL_PROGRAM), + new PublicKey(TOKEN), + ).toBase58(), + ) + assert.equal( + unsigned.poolSignerAddress, + deriveTokenPoolSignerPda( + resolveTokenPoolProgram('burn-mint'), + new PublicKey(TOKEN), + ).toBase58(), + ) + }) + + it('adds configure allowlist instruction when provided', async () => { + const unsigned = await generate({ + allowlist: [Keypair.generate().publicKey.toBase58()], + }) + + assert.equal(unsigned.instructions.length, 2) + assert.equal(unsigned.instructions[1]!.programId.toBase58(), BURN_MINT_POOL_PROGRAM) + }) + + it('uses canonical lock-release pool program', async () => { + const unsigned = await generate({ poolType: 'lock-release' }) + + assert.equal(unsigned.instructions[0]!.programId.toBase58(), LOCK_RELEASE_POOL_PROGRAM) + }) + + it('defaults authority to payer', async () => { + const unsigned = await generate({ authority: undefined }) + + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) + }) + + describe('validation', () => { + it('rejects invalid pool types', async () => { + await assert.rejects( + () => generate({ poolType: 'custom' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'deployTokenPool' && + err.context.param === 'poolType', + ) + }) + + it('rejects an empty authority', async () => { + await assert.rejects( + () => generate({ authority: '' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'deployTokenPool' && + err.context.param === 'authority', + ) + }) + + it('rejects invalid allowlist addresses', async () => { + await assert.rejects( + () => generate({ allowlist: ['not-a-pubkey'] }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'deployTokenPool' && + err.context.param === 'allowlist[0]', + ) + }) + }) + + describe('execute', () => { + it('rejects signed deploy when authority is not the wallet', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).deployTokenPool({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + wallet: WALLET, + authority: AUTHORITY, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'deployTokenPool' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.ts b/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.ts new file mode 100644 index 00000000..13f7684d --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.ts @@ -0,0 +1,180 @@ +import { type PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type TokenPoolType, + createTokenPoolProgram, + deriveTokenPoolConfigPda, + deriveTokenPoolGlobalConfigPda, + deriveTokenPoolProgramDataPda, + deriveTokenPoolSignerPda, + resolveTokenPoolProgram, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { parsePublicKey, validateAuthorityMatchesWallet, validatePoolType } from '../../validate.ts' + +/** + * Parameters for initializing a Solana token pool, optionally with an allowlist. + * + * @remarks Targets only the canonical CCIP pool programs selected by `poolType` (`burn-mint`, + * `lock-release`). Deploying a custom pool program is intentionally unsupported because this + * operation initializes pools through the SDK's bundled program IDL; custom programs may use a + * different initialize instruction or pool-state PDA layout. This is a deploy-operation scope, + * not a protocol limitation: the registry and lookup-table operations remain program-agnostic. + */ +type DeployTokenPoolParams = { + /** Token mint address this pool manages. */ + tokenAddress: string + /** Canonical token pool program to deploy: BurnMint or LockRelease. */ + poolType: TokenPoolType + /** + * Addresses to seed into the pool allowlist during initialization. + * Providing any address also enables allowlist enforcement. + * If omitted, the pool is initialized without an allowlist. + */ + allowlist?: string[] + /** Pool authority. Defaults to payer for unsigned generation and wallet public key for execute. */ + authority?: string +} + +/** Parameters for unsigned Solana token pool deploy generation. */ +export type GenerateDeployTokenPoolParams = SolanaGenerateParams + +type ParsedDeployTokenPoolParams = { + tokenMint: PublicKey + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey + allowlist: PublicKey[] +} + +/** Unsigned Solana token pool deploy result plus derived pool PDAs. */ +export type GenerateDeployTokenPoolResult = UnsignedSolanaTx & { + poolAddress: string + poolSignerAddress: string +} + +/** Parameters for executing Solana token pool deploy. */ +export type ExecuteDeployTokenPoolParams = SolanaExecuteParams + +/** Result of executing Solana token pool deploy plus derived pool PDAs. */ +export type ExecuteDeployTokenPoolResult = TransactionResult & { + poolAddress: string + poolSignerAddress: string +} + +/** Initializes a Solana token pool, optionally configuring an allowlist. */ +export class DeployTokenPool extends SolanaOperation< + DeployTokenPoolParams, + GenerateDeployTokenPoolResult, + ParsedDeployTokenPoolParams +> { + readonly name = 'deployTokenPool' + + /** Parses all public keys before any RPC. */ + protected override parse(params: GenerateDeployTokenPoolParams): ParsedDeployTokenPoolParams { + validatePoolType(this.name, 'poolType', params.poolType) + if (params.allowlist !== undefined && !Array.isArray(params.allowlist)) { + throw new CCTParamsInvalidError(this.name, 'allowlist', 'must be an array') + } + + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenMint: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram: resolveTokenPoolProgram(params.poolType), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + allowlist: (params.allowlist ?? []).map((address, i) => + parsePublicKey(this.name, `allowlist[${i}]`, address), + ), + } + } + + /** Builds the unsigned Solana token pool initialize instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedDeployTokenPoolParams, + ): Promise { + const { tokenMint, poolProgram, payer, authority, allowlist } = opts + const program = createTokenPoolProgram(chain, poolProgram, payer) + const state = deriveTokenPoolConfigPda(poolProgram, tokenMint) + const poolSigner = deriveTokenPoolSignerPda(poolProgram, tokenMint) + + const instructions = [ + await program.methods + .initialize() + .accountsStrict({ + state, + mint: tokenMint, + authority, + systemProgram: SystemProgram.programId, + program: poolProgram, + programData: deriveTokenPoolProgramDataPda(poolProgram), + config: deriveTokenPoolGlobalConfigPda(poolProgram), + }) + .instruction(), + ] + + if (allowlist.length) { + instructions.push( + await program.methods + .configureAllowList(allowlist, true) + .accountsStrict({ + state, + mint: tokenMint, + authority, + systemProgram: SystemProgram.programId, + }) + .instruction(), + ) + } + + chain.logger.debug( + `${this.name}: token = ${tokenMint.toBase58()}, poolProgram = ${poolProgram.toBase58()}`, + ) + return { + family: ChainFamily.Solana, + instructions, + mainIndex: 0, + poolAddress: state.toBase58(), + poolSignerAddress: poolSigner.toBase58(), + } + } + + /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ + override async execute( + chain: SolanaChain, + params: ExecuteDeployTokenPoolParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'deployTokenPool requires authority to be the executing wallet. Use generateUnsignedDeployTokenPool for vault-owned pools and have the vault sign/execute it.', + ) + } + + const tx = await this.buildUnsigned(chain, parsed) + const hash = await submit(chain, wallet, tx, this.name, computeUnits) + return { + ...hash, + poolAddress: tx.poolAddress, + poolSignerAddress: tx.poolSignerAddress, + } + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/edit-chain-remote-config.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/edit-chain-remote-config.test.ts new file mode 100644 index 00000000..7ce1ebdd --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/edit-chain-remote-config.test.ts @@ -0,0 +1,190 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { + deriveTokenPoolChainConfigPda, + deriveTokenPoolConfigPda, + resolveTokenPoolProgram, +} from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const SELECTOR = 5009297550715157269n +const REMOTE_TOKEN = '0x1234567890abcdef1234567890abcdef12345678' +const REMOTE_POOLS = ['0x1234567890abcdef1234567890abcdef12345678', '0xaabbccdd'] +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function chain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(), + connection: { + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(chain()).generateUnsignedEditChainRemoteConfig({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + remoteChainSelector: SELECTOR, + remoteTokenAddress: REMOTE_TOKEN, + remotePoolAddresses: REMOTE_POOLS, + remoteTokenDecimals: 18, + ...opts, + }) +} + +describe('EditChainRemoteConfig (cct/solana)', () => { + describe('generate', () => { + it('builds a padded remote-token config with remote pools', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('burn-mint') + const decoded = tokenPoolCoder.instruction.decode(instruction!.data) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction!.programId.toBase58(), poolProgram.toBase58()) + assert.deepEqual( + instruction!.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)).toBase58(), + isSigner: false, + isWritable: false, + }, + { + pubkey: deriveTokenPoolChainConfigPda( + poolProgram, + SELECTOR, + new PublicKey(TOKEN), + ).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: AUTHORITY, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId.toBase58(), isSigner: false, isWritable: false }, + ], + ) + assert.ok(decoded) + assert.equal(decoded.name, 'editChainRemoteConfig') + const data = decoded.data as { + remoteChainSelector: { toString(): string } + cfg: { + tokenAddress: { address: Buffer } + poolAddresses: { address: Buffer }[] + decimals: number + } + } + assert.equal(data.remoteChainSelector.toString(), SELECTOR.toString()) + assert.deepEqual( + data.cfg.tokenAddress.address, + Buffer.from(REMOTE_TOKEN.slice(2).padStart(64, '0'), 'hex'), + ) + assert.deepEqual( + data.cfg.poolAddresses.map(({ address }) => address), + REMOTE_POOLS.map((address) => Buffer.from(address.slice(2), 'hex')), + ) + assert.equal(data.cfg.decimals, 18) + }) + + it('supports a zero remote-chain selector', async () => { + await assert.doesNotReject(() => generate({ remoteChainSelector: 0n })) + }) + + it('uses a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await generate({ poolType: undefined, poolProgramAddress }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects invalid remote configuration values', async () => { + for (const [opts, param] of [ + [{ remoteChainSelector: 1 }, 'remoteChainSelector'], + [{ remoteChainSelector: -1n }, 'remoteChainSelector'], + [{ remoteChainSelector: 1n << 64n }, 'remoteChainSelector'], + [{ remoteTokenAddress: '0x123' }, 'remoteTokenAddress'], + [{ remotePoolAddresses: [''] }, 'remotePoolAddresses[0]'], + [{ remotePoolAddresses: ['0x123'] }, 'remotePoolAddresses[0]'], + [{ remotePoolAddresses: '0x12' }, 'remotePoolAddresses'], + [{ remoteTokenDecimals: 256 }, 'remoteTokenDecimals'], + ] as const) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).editChainRemoteConfig({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + remoteChainSelector: SELECTOR, + remoteTokenAddress: REMOTE_TOKEN, + remotePoolAddresses: REMOTE_POOLS, + remoteTokenDecimals: 18, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed editing', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).editChainRemoteConfig({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + authority: AUTHORITY, + remoteChainSelector: SELECTOR, + remoteTokenAddress: REMOTE_TOKEN, + remotePoolAddresses: REMOTE_POOLS, + remoteTokenDecimals: 18, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'editChainRemoteConfig' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/edit-chain-remote-config.ts b/ccip-sdk/src/cct/solana/token-pool/operations/edit-chain-remote-config.ts new file mode 100644 index 00000000..fc5f46f4 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/edit-chain-remote-config.ts @@ -0,0 +1,180 @@ +import { Buffer } from 'buffer' + +import { type PublicKey, SystemProgram } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type PoolProgramRef, + createTokenPoolProgram, + deriveTokenPoolChainConfigPda, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + U64_MAX, + parseHexBytes, + parseNonEmptyHexBytes, + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, + validateBigInt, + validateInteger, +} from '../../validate.ts' + +/** Parameters shared by Solana token pool remote-config editing generation and execution. */ +type EditChainRemoteConfigParams = PoolProgramRef & { + /** Token mint address managed by the local pool. */ + tokenAddress: string + /** CCIP selector of the remote chain (`u64`). */ + remoteChainSelector: bigint + /** Hex-encoded remote token address, optionally `0x`-prefixed, up to 32 bytes. Left-padded in the instruction. */ + remoteTokenAddress: string + /** + * Hex-encoded remote pool addresses, optionally `0x`-prefixed. Stored at native byte length; + * unlike `remoteTokenAddress`, they are not left-padded. + */ + remotePoolAddresses: string[] + /** Remote token decimals (`u8`): an integer from 0 to 255; 0 is valid. */ + remoteTokenDecimals: number + /** Pool owner. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedEditChainRemoteConfigParams = { + tokenAddress: PublicKey + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey + remoteChainSelector: bigint + remoteTokenAddress: Buffer + remotePoolAddresses: Buffer[] + remoteTokenDecimals: number +} + +/** Parameters for unsigned Solana token pool remote configuration editing. */ +export type GenerateEditChainRemoteConfigParams = SolanaGenerateParams + +/** Unsigned Solana token pool remote configuration editing result. */ +export type GenerateEditChainRemoteConfigResult = UnsignedSolanaTx + +/** Parameters for executing Solana token pool remote configuration editing. */ +export type ExecuteEditChainRemoteConfigParams = SolanaExecuteParams + +/** Result of executing Solana token pool remote configuration editing. */ +export type ExecuteEditChainRemoteConfigResult = TransactionResult + +/** + * Replaces an initialized remote-chain config. + * + * @remarks + * Full replacement, not a partial update — pass the complete intended config for all three fields, + * or omitted values are cleared. For example, `remotePoolAddresses: []` clears all remote pools. + */ +export class EditChainRemoteConfig extends SolanaOperation< + EditChainRemoteConfigParams, + UnsignedSolanaTx, + ParsedEditChainRemoteConfigParams +> { + readonly name = 'editChainRemoteConfig' + + /** Parses config values and defaults authority to payer without mutating caller params. */ + protected override parse( + params: GenerateEditChainRemoteConfigParams, + ): ParsedEditChainRemoteConfigParams { + validateBigInt(this.name, 'remoteChainSelector', params.remoteChainSelector, 0n, U64_MAX) + validateInteger(this.name, 'remoteTokenDecimals', params.remoteTokenDecimals, 0, 255) + + const remoteTokenAddress = parseHexBytes( + this.name, + 'remoteTokenAddress', + params.remoteTokenAddress, + 32, + ) + + if (!Array.isArray(params.remotePoolAddresses)) { + throw new CCTParamsInvalidError(this.name, 'remotePoolAddresses', 'must be an array') + } + const remotePoolAddresses = params.remotePoolAddresses.map((address, i) => + parseNonEmptyHexBytes(this.name, `remotePoolAddresses[${i}]`, address), + ) + + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram: resolvePoolProgram(this.name, params), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + remoteChainSelector: params.remoteChainSelector, + remoteTokenAddress, + remotePoolAddresses, + remoteTokenDecimals: params.remoteTokenDecimals, + } + } + + /** Builds the unsigned Solana `editChainRemoteConfig` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedEditChainRemoteConfigParams, + ): Promise { + const program = createTokenPoolProgram(chain, opts.poolProgram, opts.payer) + const state = deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress) + const chainConfig = deriveTokenPoolChainConfigPda( + opts.poolProgram, + opts.remoteChainSelector, + opts.tokenAddress, + ) + const paddedRemoteToken = Buffer.alloc(32) + opts.remoteTokenAddress.copy(paddedRemoteToken, 32 - opts.remoteTokenAddress.length) + + const instruction = await program.methods + .editChainRemoteConfig(new BN(opts.remoteChainSelector.toString()), opts.tokenAddress, { + tokenAddress: { address: paddedRemoteToken }, + poolAddresses: opts.remotePoolAddresses.map((address) => ({ address })), + decimals: opts.remoteTokenDecimals, + }) + .accountsStrict({ + state, + chainConfig, + authority: opts.authority, + systemProgram: SystemProgram.programId, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}, remoteChainSelector = ${opts.remoteChainSelector}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the pool owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteEditChainRemoteConfigParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'editChainRemoteConfig requires authority to be the executing wallet. Use generateUnsignedEditChainRemoteConfig for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-remotes.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-remotes.test.ts new file mode 100644 index 00000000..83dabc7c --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-remotes.test.ts @@ -0,0 +1,90 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey } from '@solana/web3.js' + +import type { TokenPoolRemote } from '../../../../chain.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' + +function key(byte: number): PublicKey { + return new PublicKey(Uint8Array.from({ length: 32 }, () => byte)) +} + +const REMOTES: Record = { + 'ethereum-mainnet': { + remoteToken: '0x1234', + remotePools: ['0x5678'], + inboundRateLimiterState: { tokens: 25n, capacity: 50n, rate: 5n }, + outboundRateLimiterState: null, + }, +} + +describe('GetTokenPoolRemotes (cct/solana)', () => { + const mint = key(2) + const program = key(3) + const selector = 5009297550715157269n + + function chain(): SolanaChain { + return { + getTokenPoolRemotes: async (state: string, remoteChainSelector?: bigint) => { + assert.equal(state, deriveTokenPoolConfigPda(program, mint).toBase58()) + assert.equal(remoteChainSelector, selector) + return REMOTES + }, + } as unknown as SolanaChain + } + + describe('query', () => { + it('delegates selected remote config decoding to the chain reader', async () => { + const remotes = await SolanaTokenManager.fromChain(chain()).getTokenPoolRemotes({ + tokenAddress: mint.toBase58(), + poolProgramAddress: program.toBase58(), + remoteChainSelector: selector, + }) + + assert.equal(remotes, REMOTES) + }) + + it('omits the selector to read all remote configs', async () => { + const chainWithAll = { + getTokenPoolRemotes: async (_state: string, remoteChainSelector?: bigint) => { + assert.equal(remoteChainSelector, undefined) + return REMOTES + }, + } as unknown as SolanaChain + + const remotes = await SolanaTokenManager.fromChain(chainWithAll).getTokenPoolRemotes({ + tokenAddress: mint.toBase58(), + poolProgramAddress: program.toBase58(), + }) + + assert.equal(remotes, REMOTES) + }) + }) + + describe('validation', () => { + it('validates the token address and optional remote selector before reading', async () => { + const cases: Array<[Partial<{ tokenAddress: string; remoteChainSelector: bigint }>, string]> = + [ + [{ tokenAddress: 'invalid' }, 'tokenAddress'], + [{ remoteChainSelector: -1n }, 'remoteChainSelector'], + [{ remoteChainSelector: 1 as never }, 'remoteChainSelector'], + ] + for (const [opts, param] of cases) { + await assert.rejects( + SolanaTokenManager.fromChain(chain()).getTokenPoolRemotes({ + tokenAddress: mint.toBase58(), + poolProgramAddress: program.toBase58(), + remoteChainSelector: selector, + ...opts, + }), + (error: unknown) => + error instanceof CCTParamsInvalidError && error.context.param === param, + ) + } + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-remotes.ts b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-remotes.ts new file mode 100644 index 00000000..4cef282f --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-remotes.ts @@ -0,0 +1,57 @@ +import type { PublicKey } from '@solana/web3.js' + +import type { TokenPoolRemote } from '../../../../chain.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { type PoolProgramRef, deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' +import { SolanaQuery } from '../../query.ts' +import { U64_MAX, parsePublicKey, resolvePoolProgram, validateBigInt } from '../../validate.ts' + +/** Parameters for reading Solana token pool remote-chain configurations. */ +export type GetTokenPoolRemotesParams = PoolProgramRef & { + /** Token mint address managed by the local pool. */ + tokenAddress: string + /** Optional CCIP selector of the destination chain to read (`u64`). */ + remoteChainSelector?: bigint +} + +/** Remote-chain configurations keyed by network name. */ +export type GetTokenPoolRemotesResult = Record + +/** {@link GetTokenPoolRemotesParams} with its mint and pool program resolved to public keys. */ +type ParsedGetTokenPoolRemotesParams = GetTokenPoolRemotesParams & { + mint: PublicKey + programId: PublicKey +} + +/** Reads all, or one selected, remote-chain configurations of a Solana token pool. */ +export class GetTokenPoolRemotes extends SolanaQuery< + GetTokenPoolRemotesParams, + GetTokenPoolRemotesResult, + ParsedGetTokenPoolRemotesParams +> { + readonly name = 'getTokenPoolRemotes' + + /** + * Converts the mint and pool program, and validates the optional remote-chain selector. + * @throws {@link CCTParamsInvalidError} if a pool parameter or selector is invalid. + */ + protected prepare(params: GetTokenPoolRemotesParams): ParsedGetTokenPoolRemotesParams { + if (params.remoteChainSelector !== undefined) { + validateBigInt(this.name, 'remoteChainSelector', params.remoteChainSelector, 0n, U64_MAX) + } + return { + ...params, + mint: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + programId: resolvePoolProgram(this.name, params), + } + } + + /** Derives the pool state PDA and delegates remote config decoding to the shared chain reader. */ + protected read( + chain: SolanaChain, + { mint, programId, remoteChainSelector }: ParsedGetTokenPoolRemotesParams, + ): Promise { + const state = deriveTokenPoolConfigPda(programId, mint).toBase58() + return chain.getTokenPoolRemotes(state, remoteChainSelector) + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.test.ts new file mode 100644 index 00000000..2eff1db3 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.test.ts @@ -0,0 +1,171 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { BorshAccountsCoder } from '@coral-xyz/anchor' +import { PublicKey } from '@solana/web3.js' + +import { GetTokenPoolState } from './get-token-pool-state.ts' +import { CCIPTokenPoolStateNotFoundError } from '../../../../errors/index.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTDataDecodeError } from '../../../errors.ts' +import { decodeTokenPoolState, deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' + +function key(byte: number): PublicKey { + return new PublicKey(Uint8Array.from({ length: 32 }, () => byte)) +} + +function stateData(mint: PublicKey): Buffer { + return Buffer.concat([ + BorshAccountsCoder.accountDiscriminator('State'), + Buffer.from([1]), + key(3).toBuffer(), + mint.toBuffer(), + Buffer.from([6]), + key(4).toBuffer(), + key(5).toBuffer(), + key(6).toBuffer(), + key(7).toBuffer(), + key(8).toBuffer(), + key(9).toBuffer(), + key(10).toBuffer(), + key(11).toBuffer(), + Buffer.from([1, 1]), + Buffer.from([2, 0, 0, 0]), + key(12).toBuffer(), + key(13).toBuffer(), + key(14).toBuffer(), + ]) +} + +describe('GetTokenPoolState (cct/solana)', () => { + describe('query', () => { + it('returns decoded state fields', async () => { + const mint = key(2) + const chain = { + connection: { getAccountInfo: async () => ({ owner: key(1), data: stateData(mint) }) }, + } as unknown as SolanaChain + + const getTokenPoolState = new GetTokenPoolState() + const lockRelease = await getTokenPoolState.query(chain, { + poolType: 'lock-release', + tokenAddress: mint.toBase58(), + }) + const burnMint = await getTokenPoolState.query(chain, { + poolType: 'burn-mint', + tokenAddress: mint.toBase58(), + }) + const customProgram = key(15).toBase58() + const custom = await getTokenPoolState.query(chain, { + poolProgramAddress: customProgram, + tokenAddress: mint.toBase58(), + }) + + assert.equal(lockRelease.version, 1) + assert.equal(lockRelease.config.mint, mint.toBase58()) + assert.equal(lockRelease.config.decimals, 6) + // the op resolves to the union; the facade's overloads are what narrow for callers + assert.ok('canAcceptLiquidity' in lockRelease.config) + assert.equal(lockRelease.config.canAcceptLiquidity, true) + assert.equal(lockRelease.config.listEnabled, true) + assert.deepEqual(lockRelease.config.allowList, [key(12).toBase58(), key(13).toBase58()]) + assert.equal(lockRelease.config.rmnRemote, key(14).toBase58()) + assert.ok(!('rebalancer' in burnMint.config)) + assert.ok(!('canAcceptLiquidity' in burnMint.config)) + assert.equal(custom.programId, customProgram) + assert.equal(custom.config.mint, mint.toBase58()) + }) + + it('wraps decode failures with pool context', async () => { + const mint = key(2).toBase58() + const poolProgram = key(15).toBase58() + const chain = { + connection: { getAccountInfo: async () => ({ owner: key(1), data: Buffer.alloc(8) }) }, + } as unknown as SolanaChain + + await assert.rejects( + new GetTokenPoolState().query(chain, { + tokenAddress: mint, + poolProgramAddress: poolProgram, + }), + (error: unknown) => { + assert.ok(error instanceof CCTDataDecodeError) + assert.equal( + error.context.account, + deriveTokenPoolConfigPda(new PublicKey(poolProgram), new PublicKey(mint)).toBase58(), + ) + assert.equal(error.context.mint, mint) + assert.equal(error.context.poolProgram, poolProgram) + assert.equal(error.context.accountOwner, key(1).toBase58()) + assert.ok(error.cause instanceof Error) + return true + }, + ) + }) + + it('wraps non-Error decode causes', (t) => { + t.mock.method(tokenPoolCoder.accounts, 'decode', () => { + // eslint-disable-next-line @typescript-eslint/only-throw-error -- verify unknown decoder throws are normalized. + throw 'invalid account data' + }) + + assert.throws( + () => + decodeTokenPoolState(Buffer.alloc(8), { + tokenPool: key(1).toBase58(), + mint: key(2).toBase58(), + poolProgram: key(3).toBase58(), + accountOwner: key(4).toBase58(), + }), + (error: unknown) => { + assert.ok(error instanceof CCTDataDecodeError) + assert.ok(error.cause instanceof Error) + assert.equal(error.cause.message, 'invalid account data') + return true + }, + ) + }) + + it('includes the mint and program in missing-state context', async () => { + const mint = key(2).toBase58() + const poolProgram = key(15).toBase58() + const chain = { + connection: { getAccountInfo: async () => null }, + } as unknown as SolanaChain + + await assert.rejects( + new GetTokenPoolState().query(chain, { + tokenAddress: mint, + poolProgramAddress: poolProgram, + }), + (error: unknown) => { + assert.ok(error instanceof CCIPTokenPoolStateNotFoundError) + assert.match(error.message, /^TokenPool State PDA not found at /) + assert.equal(error.context.mint, mint) + assert.equal(error.context.poolProgram, poolProgram) + return true + }, + ) + }) + }) + + describe('validation', () => { + it('requires exactly one pool program reference', async () => { + const getTokenPoolState = new GetTokenPoolState() + const tokenAddress = key(2).toBase58() + const poolProgramAddress = key(15).toBase58() + + await assert.rejects( + getTokenPoolState.query( + {} as SolanaChain, + { + tokenAddress, + poolType: 'burn-mint', + poolProgramAddress, + } as never, + ), + ) + await assert.rejects(getTokenPoolState.query({} as SolanaChain, { tokenAddress } as never)) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.ts b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.ts new file mode 100644 index 00000000..3b005aeb --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.ts @@ -0,0 +1,162 @@ +import type { PublicKey } from '@solana/web3.js' + +import { CCIPTokenPoolStateNotFoundError } from '../../../../errors/index.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { + type PoolProgramRef, + type TokenPoolConfig, + decodeTokenPoolState, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { SolanaQuery } from '../../query.ts' +import { parsePublicKey, resolvePoolProgram } from '../../validate.ts' + +export type { + BurnMintPoolProgramRef, + CustomPoolProgramRef, + LockReleasePoolProgramRef, + PoolProgramRef, +} from '../../programs/token-pool.ts' + +/** Parameters for reading a Solana token pool state. */ +export type GetTokenPoolStateParams = PoolProgramRef & { + tokenAddress: string +} + +type BaseConfig = { + tokenProgram: string + mint: string + decimals: number + poolSigner: string + poolTokenAccount: string + owner: string + proposedOwner: string + rateLimitAdmin: string + routerOnrampAuthority: string + router: string + listEnabled: boolean + allowList: string[] + rmnRemote: string +} + +type GetTokenPoolStateResultBase = { + stateAddress: string + /** Resolved pool program address: canonical for `poolType`, supplied for `poolProgramAddress`. */ + programId: string + version: number +} + +/** State returned for a burn-mint or custom token pool program. */ +export type BaseGetTokenPoolStateResult = GetTokenPoolStateResultBase & { + config: BaseConfig +} + +/** State returned for a lock-release token pool program. */ +export type LockReleaseGetTokenPoolStateResult = GetTokenPoolStateResultBase & { + config: BaseConfig & { + rebalancer: string + canAcceptLiquidity: boolean + } +} + +/** + * State returned for a canonical or custom token pool program. + * + * Reads queried with `poolProgramAddress` use the base config shape and omit lock-release-only + * fields, even when the supplied address is the lock-release program. The + * {@link SolanaTokenManager.getTokenPoolState} overloads pick the arm per pool type, so callers + * only narrow this union when the program is not known statically. + */ +export type GetTokenPoolStateResult = + BaseGetTokenPoolStateResult | LockReleaseGetTokenPoolStateResult + +function serializeBaseConfig(config: TokenPoolConfig): BaseConfig { + return { + tokenProgram: config.tokenProgram.toBase58(), + mint: config.mint.toBase58(), + decimals: config.decimals, + poolSigner: config.poolSigner.toBase58(), + poolTokenAccount: config.poolTokenAccount.toBase58(), + owner: config.owner.toBase58(), + proposedOwner: config.proposedOwner.toBase58(), + rateLimitAdmin: config.rateLimitAdmin.toBase58(), + routerOnrampAuthority: config.routerOnrampAuthority.toBase58(), + router: config.router.toBase58(), + listEnabled: config.listEnabled, + allowList: config.allowList.map((address) => address.toBase58()), + rmnRemote: config.rmnRemote.toBase58(), + } +} + +/** {@link GetTokenPoolStateParams} with its mint and pool program resolved to public keys. */ +type ParsedGetTokenPoolStateParams = GetTokenPoolStateParams & { + mint: PublicKey + programId: PublicKey +} + +/** Reads the complete state of a Solana token pool. */ +export class GetTokenPoolState extends SolanaQuery< + GetTokenPoolStateParams, + GetTokenPoolStateResult, + ParsedGetTokenPoolStateParams +> { + readonly name = 'getTokenPoolState' + + /** + * Converts the mint and resolves the pool program. + * @throws {@link CCTParamsInvalidError} if `tokenAddress` is not a public key, or if the pool + * program is identified by neither or both of `poolType` / `poolProgramAddress` + */ + protected prepare(params: GetTokenPoolStateParams): ParsedGetTokenPoolStateParams { + return { + ...params, + mint: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + programId: resolvePoolProgram(this.name, params), + } + } + + /** Reads and serializes the token pool config account; the facade's overloads narrow the arm. */ + protected async read( + chain: SolanaChain, + params: ParsedGetTokenPoolStateParams, + ): Promise { + const { mint, programId } = params + const state = deriveTokenPoolConfigPda(programId, mint) + + const account = await chain.connection.getAccountInfo(state) + if (!account) { + throw new CCIPTokenPoolStateNotFoundError(state.toBase58(), { + context: { + mint: params.tokenAddress, + poolProgram: programId.toBase58(), + }, + }) + } + + const { version, config } = decodeTokenPoolState(account.data, { + tokenPool: state.toBase58(), + mint: params.tokenAddress, + poolProgram: programId.toBase58(), + accountOwner: account.owner.toBase58(), + }) + const result = { + stateAddress: state.toBase58(), + programId: programId.toBase58(), + version, + } + const baseConfig = serializeBaseConfig(config) + + if (params.poolType === 'lock-release') { + return { + ...result, + config: { + ...baseConfig, + rebalancer: config.rebalancer.toBase58(), + canAcceptLiquidity: config.canAcceptLiquidity, + }, + } + } + + return { ...result, config: baseConfig } + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts new file mode 100644 index 00000000..c01db641 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -0,0 +1,15 @@ +export * from './accept-ownership.ts' +export * from './append-remote-pool-addresses.ts' +export * from './apply-chain-updates.ts' +export * from './configure-allowlist.ts' +export * from './create-token-multisig.ts' +export * from './deploy-token-pool.ts' +export * from './delete-chain-remote-config.ts' +export * from './edit-chain-remote-config.ts' +export * from './get-token-pool-remotes.ts' +export * from './get-token-pool-state.ts' +export * from './init-chain-remote-config.ts' +export * from './remove-from-allowlist.ts' +export * from './set-chain-rate-limit.ts' +export * from './set-rate-limit-admin.ts' +export * from './transfer-ownership.ts' diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/init-chain-remote-config.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/init-chain-remote-config.test.ts new file mode 100644 index 00000000..972465b5 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/init-chain-remote-config.test.ts @@ -0,0 +1,179 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { + deriveTokenPoolChainConfigPda, + deriveTokenPoolConfigPda, + resolveTokenPoolProgram, +} from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const SELECTOR = 5009297550715157269n +const REMOTE_TOKEN = '0x1234567890abcdef1234567890abcdef12345678' +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function chain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(), + connection: { + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(chain()).generateUnsignedInitChainRemoteConfig({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + remoteChainSelector: SELECTOR, + remoteTokenAddress: REMOTE_TOKEN, + remoteTokenDecimals: 18, + ...opts, + }) +} + +describe('InitChainRemoteConfig (cct/solana)', () => { + describe('generate', () => { + it('builds a padded remote-token config with no remote pools', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('burn-mint') + const decoded = tokenPoolCoder.instruction.decode(instruction!.data) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction!.programId.toBase58(), poolProgram.toBase58()) + assert.deepEqual( + instruction!.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)).toBase58(), + isSigner: false, + isWritable: false, + }, + { + pubkey: deriveTokenPoolChainConfigPda( + poolProgram, + SELECTOR, + new PublicKey(TOKEN), + ).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: AUTHORITY, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId.toBase58(), isSigner: false, isWritable: false }, + ], + ) + assert.ok(decoded) + assert.equal(decoded.name, 'initChainRemoteConfig') + const data = decoded.data as { + remoteChainSelector: { toString(): string } + cfg: { tokenAddress: { address: Buffer }; poolAddresses: unknown[]; decimals: number } + } + assert.equal(data.remoteChainSelector.toString(), SELECTOR.toString()) + assert.deepEqual( + data.cfg.tokenAddress.address, + Buffer.from(REMOTE_TOKEN.slice(2).padStart(64, '0'), 'hex'), + ) + assert.deepEqual(data.cfg.poolAddresses, []) + assert.equal(data.cfg.decimals, 18) + }) + + it('supports a zero remote-chain selector', async () => { + await assert.doesNotReject(() => generate({ remoteChainSelector: 0n })) + }) + + it('uses a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await generate({ poolType: undefined, poolProgramAddress }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects invalid remote configuration values', async () => { + for (const [opts, param] of [ + [{ remoteChainSelector: 1 }, 'remoteChainSelector'], + [{ remoteChainSelector: -1n }, 'remoteChainSelector'], + [{ remoteChainSelector: 1n << 64n }, 'remoteChainSelector'], + [{ remoteTokenAddress: '' }, 'remoteTokenAddress'], + [{ remoteTokenAddress: '0x' }, 'remoteTokenAddress'], + [{ remoteTokenAddress: '0x123' }, 'remoteTokenAddress'], + [{ remoteTokenAddress: null }, 'remoteTokenAddress'], + [{ remoteTokenDecimals: 256 }, 'remoteTokenDecimals'], + ] as const) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).initChainRemoteConfig({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + remoteChainSelector: SELECTOR, + remoteTokenAddress: REMOTE_TOKEN, + remoteTokenDecimals: 18, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed initialization', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).initChainRemoteConfig({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + authority: AUTHORITY, + remoteChainSelector: SELECTOR, + remoteTokenAddress: REMOTE_TOKEN, + remoteTokenDecimals: 18, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'initChainRemoteConfig' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/init-chain-remote-config.ts b/ccip-sdk/src/cct/solana/token-pool/operations/init-chain-remote-config.ts new file mode 100644 index 00000000..03df8f04 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/init-chain-remote-config.ts @@ -0,0 +1,167 @@ +import { Buffer } from 'buffer' + +import { type PublicKey, SystemProgram } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type PoolProgramRef, + createTokenPoolProgram, + deriveTokenPoolChainConfigPda, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + U64_MAX, + parseHexBytes, + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, + validateBigInt, + validateInteger, +} from '../../validate.ts' + +/** Parameters shared by Solana token pool remote-config initialization generation and execution. */ +type InitChainRemoteConfigParams = PoolProgramRef & { + /** Token mint address managed by the local pool. */ + tokenAddress: string + /** CCIP selector of the remote chain (`u64`). */ + remoteChainSelector: bigint + /** Hex-encoded remote token address, optionally `0x`-prefixed, up to 32 bytes. Left-padded in the instruction. */ + remoteTokenAddress: string + /** Decimals of the remote token (`u8`), not the local mint: an integer from 0 to 255; 0 is valid. */ + remoteTokenDecimals: number + /** Pool owner. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedInitChainRemoteConfigParams = { + tokenAddress: PublicKey + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey + remoteChainSelector: bigint + remoteTokenAddress: Buffer + remoteTokenDecimals: number +} + +/** Parameters for unsigned Solana token pool remote configuration initialization. */ +export type GenerateInitChainRemoteConfigParams = SolanaGenerateParams + +/** Unsigned Solana token pool remote configuration initialization result. */ +export type GenerateInitChainRemoteConfigResult = UnsignedSolanaTx + +/** Parameters for executing Solana token pool remote configuration initialization. */ +export type ExecuteInitChainRemoteConfigParams = SolanaExecuteParams + +/** Result of executing Solana token pool remote configuration initialization. */ +export type ExecuteInitChainRemoteConfigResult = TransactionResult + +/** + * Initializes a previously unconfigured remote-chain config. + * + * @remarks Fails if the chain config already exists. + */ +export class InitChainRemoteConfig extends SolanaOperation< + InitChainRemoteConfigParams, + UnsignedSolanaTx, + ParsedInitChainRemoteConfigParams +> { + readonly name = 'initChainRemoteConfig' + + /** Parses config values and defaults authority to payer without mutating caller params. */ + protected override parse( + params: GenerateInitChainRemoteConfigParams, + ): ParsedInitChainRemoteConfigParams { + validateBigInt(this.name, 'remoteChainSelector', params.remoteChainSelector, 0n, U64_MAX) + validateInteger(this.name, 'remoteTokenDecimals', params.remoteTokenDecimals, 0, 255) + + const remoteTokenAddress = parseHexBytes( + this.name, + 'remoteTokenAddress', + params.remoteTokenAddress, + 32, + ) + + if (!remoteTokenAddress.length) { + throw new CCTParamsInvalidError(this.name, 'remoteTokenAddress', 'must not be empty') + } + + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram: resolvePoolProgram(this.name, params), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + remoteChainSelector: params.remoteChainSelector, + remoteTokenAddress, + remoteTokenDecimals: params.remoteTokenDecimals, + } + } + + /** Builds the unsigned Solana `initChainRemoteConfig` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedInitChainRemoteConfigParams, + ): Promise { + const program = createTokenPoolProgram(chain, opts.poolProgram, opts.payer) + const state = deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress) + const chainConfig = deriveTokenPoolChainConfigPda( + opts.poolProgram, + opts.remoteChainSelector, + opts.tokenAddress, + ) + const paddedRemoteToken = Buffer.alloc(32) + opts.remoteTokenAddress.copy(paddedRemoteToken, 32 - opts.remoteTokenAddress.length) + + const instruction = await program.methods + .initChainRemoteConfig(new BN(opts.remoteChainSelector.toString()), opts.tokenAddress, { + tokenAddress: { address: paddedRemoteToken }, + poolAddresses: [], + decimals: opts.remoteTokenDecimals, + }) + .accountsStrict({ + state, + chainConfig, + authority: opts.authority, + systemProgram: SystemProgram.programId, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}, remoteChainSelector = ${opts.remoteChainSelector}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the pool owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteInitChainRemoteConfigParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'initChainRemoteConfig requires authority to be the executing wallet. Use generateUnsignedInitChainRemoteConfig for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/remove-from-allowlist.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/remove-from-allowlist.test.ts new file mode 100644 index 00000000..3e9cb586 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/remove-from-allowlist.test.ts @@ -0,0 +1,193 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveTokenPoolConfigPda, resolveTokenPoolProgram } from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const ALLOWED = Keypair.generate().publicKey.toBase58() +const SECOND_ALLOWED = Keypair.generate().publicKey.toBase58() +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...stubChain(), + connection: { + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedRemoveFromAllowlist({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + remove: [ALLOWED], + ...opts, + }) +} + +describe('RemoveFromAllowlist (cct/solana)', () => { + describe('generate', () => { + it('builds an unsigned remove from allowlist instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('burn-mint') + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction.programId.toBase58(), poolProgram.toBase58()) + assert.equal( + instruction.data.subarray(0, 8).toString('hex'), + createHash('sha256').update('global:remove_from_allow_list').digest('hex').slice(0, 16), + ) + assert.deepEqual( + instruction.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: TOKEN, isSigner: false, isWritable: false }, + { pubkey: AUTHORITY, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId.toBase58(), isSigner: false, isWritable: false }, + ], + ) + }) + + it('encodes multiple addresses', async () => { + const unsigned = await generate({ remove: [ALLOWED, SECOND_ALLOWED] }) + const decoded = tokenPoolCoder.instruction.decode(unsigned.instructions[0]!.data) + + assert.ok(decoded) + assert.equal(decoded.name, 'removeFromAllowList') + assert.deepEqual( + (decoded.data as { remove: PublicKey[] }).remove.map((address) => address.toBase58()), + [ALLOWED, SECOND_ALLOWED], + ) + }) + + it('uses a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await SolanaTokenManager.fromChain( + stubChain(), + ).generateUnsignedRemoveFromAllowlist({ + tokenAddress: TOKEN, + poolProgramAddress, + payer: PAYER, + authority: AUTHORITY, + remove: [ALLOWED], + }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects invalid pool program references', async () => { + await assert.rejects( + () => generate({ poolType: 'custom' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'removeFromAllowlist' && + err.context.param === 'poolType', + ) + }) + + it('rejects an empty or non-array removal list', async () => { + for (const remove of [[], 'not-an-array']) { + await assert.rejects( + () => generate({ remove }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'removeFromAllowlist' && + err.context.param === 'remove', + ) + } + }) + + it('rejects invalid removal addresses', async () => { + await assert.rejects( + () => generate({ remove: ['not-a-pubkey'] }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'removeFromAllowlist' && + err.context.param === 'remove[0]', + ) + }) + + it('rejects duplicate removal addresses', async () => { + await assert.rejects( + () => generate({ remove: [ALLOWED, ALLOWED] }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'removeFromAllowlist' && + err.context.param === 'remove', + ) + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).removeFromAllowlist({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + remove: [ALLOWED], + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed removal', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).removeFromAllowlist({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + authority: AUTHORITY, + remove: [ALLOWED], + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'removeFromAllowlist' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/remove-from-allowlist.ts b/ccip-sdk/src/cct/solana/token-pool/operations/remove-from-allowlist.ts new file mode 100644 index 00000000..42a4f2de --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/remove-from-allowlist.ts @@ -0,0 +1,145 @@ +import { type PublicKey, SystemProgram } from '@solana/web3.js' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { type UnsignedSolanaTx, isWallet } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type PoolProgramRef, + createTokenPoolProgram, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, +} from '../../validate.ts' + +/** Parameters shared by Solana token pool `removeFromAllowlist` generation and execution. */ +type RemoveFromAllowlistParams = PoolProgramRef & { + /** Token mint address managed by the pool. */ + tokenAddress: string + /** + * Addresses to remove from the pool allowlist. Must be non-empty and contain no duplicates. + * Every address must currently be allowlisted; if any is absent, the program reverts the entire + * removal. + */ + remove: string[] + /** Pool owner. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedRemoveFromAllowlistParams = { + tokenAddress: PublicKey + poolProgram: PublicKey + remove: PublicKey[] + payer: PublicKey + authority: PublicKey +} + +/** Parameters for unsigned Solana token pool allowlist removal. */ +export type GenerateRemoveFromAllowlistParams = SolanaGenerateParams + +/** Unsigned Solana token pool allowlist removal result. */ +export type GenerateRemoveFromAllowlistResult = UnsignedSolanaTx + +/** Parameters for executing Solana token pool allowlist removal. */ +export type ExecuteRemoveFromAllowlistParams = SolanaExecuteParams + +/** Result of executing Solana token pool allowlist removal. */ +export type ExecuteRemoveFromAllowlistResult = TransactionResult + +/** Removes addresses from a Solana token pool allowlist. */ +export class RemoveFromAllowlist extends SolanaOperation< + RemoveFromAllowlistParams, + UnsignedSolanaTx, + ParsedRemoveFromAllowlistParams +> { + readonly name = 'removeFromAllowlist' + + /** Parses public keys and defaults authority to payer without mutating caller params. */ + protected override parse( + params: GenerateRemoveFromAllowlistParams, + ): ParsedRemoveFromAllowlistParams { + if (!Array.isArray(params.remove) || params.remove.length === 0) { + throw new CCTParamsInvalidError(this.name, 'remove', 'must be a non-empty array') + } + + const remove = params.remove.map((address, index) => + parsePublicKey(this.name, `remove[${index}]`, address), + ) + if (new Set(remove.map((address) => address.toBase58())).size !== remove.length) { + throw new CCTParamsInvalidError(this.name, 'remove', 'must not contain duplicate addresses') + } + + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram: resolvePoolProgram(this.name, params), + remove, + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + } + } + + /** Builds the unsigned Solana `removeFromAllowList` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedRemoveFromAllowlistParams, + ): Promise { + const program = createTokenPoolProgram(chain, opts.poolProgram, opts.payer) + const state = deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress) + + const instruction = await program.methods + .removeFromAllowList(opts.remove) + .accountsStrict({ + state, + mint: opts.tokenAddress, + authority: opts.authority, + systemProgram: SystemProgram.programId, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the pool owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteRemoveFromAllowlistParams, + ): Promise { + const { wallet, computeUnits, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const generateParams: GenerateRemoveFromAllowlistParams = { + ...rest, + payer: wallet.publicKey.toBase58(), + } + const parsed = this.prepare(generateParams) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'removeFromAllowlist requires authority to be the executing wallet. Use generateUnsignedRemoveFromAllowlist for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/set-chain-rate-limit.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/set-chain-rate-limit.test.ts new file mode 100644 index 00000000..7d322eeb --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/set-chain-rate-limit.test.ts @@ -0,0 +1,228 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { + deriveTokenPoolChainConfigPda, + deriveTokenPoolConfigPda, + resolveTokenPoolProgram, +} from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const SELECTOR = 5009297550715157269n +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function chain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(), + connection: { + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(chain()).generateUnsignedSetChainRateLimit({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + remoteChainSelector: SELECTOR, + inbound: { enabled: true, capacity: 100n, rate: 10n }, + outbound: { enabled: true, capacity: 200n, rate: 20n }, + ...opts, + }) +} + +describe('SetChainRateLimit (cct/solana)', () => { + describe('generate', () => { + it('builds the set-chain-rate-limit instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('burn-mint') + const decoded = tokenPoolCoder.instruction.decode(instruction!.data) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction!.programId.toBase58(), poolProgram.toBase58()) + assert.deepEqual( + instruction!.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)).toBase58(), + isSigner: false, + isWritable: false, + }, + { + pubkey: deriveTokenPoolChainConfigPda( + poolProgram, + SELECTOR, + new PublicKey(TOKEN), + ).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: AUTHORITY, isSigner: true, isWritable: true }, + ], + ) + assert.ok(decoded) + assert.equal(decoded.name, 'setChainRateLimit') + const data = decoded.data as { + remoteChainSelector: { toString(): string } + mint: PublicKey + inbound: { + enabled: boolean + capacity: { toString(): string } + rate: { toString(): string } + } + outbound: { + enabled: boolean + capacity: { toString(): string } + rate: { toString(): string } + } + } + assert.equal(data.remoteChainSelector.toString(), SELECTOR.toString()) + assert.equal(data.mint.toBase58(), TOKEN) + assert.equal(data.inbound.enabled, true) + assert.equal(data.inbound.capacity.toString(), '100') + assert.equal(data.inbound.rate.toString(), '10') + assert.equal(data.outbound.enabled, true) + assert.equal(data.outbound.capacity.toString(), '200') + assert.equal(data.outbound.rate.toString(), '20') + }) + + it('encodes each enabled and disabled direction combination', async () => { + for (const [inbound, outbound, expected] of [ + [{ enabled: false }, { enabled: false }, [false, '0', '0', false, '0', '0']], + [ + { enabled: false, capacity: 0n, rate: 0n }, + { enabled: true, capacity: 200n, rate: 20n }, + [false, '0', '0', true, '200', '20'], + ], + [ + { enabled: true, capacity: 100n, rate: 10n }, + { enabled: false }, + [true, '100', '10', false, '0', '0'], + ], + ] as const) { + const unsigned = await generate({ remoteChainSelector: 0n, inbound, outbound }) + const decoded = tokenPoolCoder.instruction.decode(unsigned.instructions[0]!.data) + assert.ok(decoded) + const data = decoded.data as { + remoteChainSelector: { toString(): string } + inbound: { + enabled: boolean + capacity: { toString(): string } + rate: { toString(): string } + } + outbound: { + enabled: boolean + capacity: { toString(): string } + rate: { toString(): string } + } + } + + assert.equal(data.remoteChainSelector.toString(), '0') + assert.deepEqual( + [ + data.inbound.enabled, + data.inbound.capacity.toString(), + data.inbound.rate.toString(), + data.outbound.enabled, + data.outbound.capacity.toString(), + data.outbound.rate.toString(), + ], + expected, + ) + } + }) + + it('uses a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await generate({ poolType: undefined, poolProgramAddress }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects invalid rate-limit values', async () => { + for (const [opts, param] of [ + [{ remoteChainSelector: 1 }, 'remoteChainSelector'], + [{ inbound: { enabled: true, capacity: -1n, rate: 1n } }, 'inbound.capacity'], + [{ outbound: { enabled: true, capacity: 1n, rate: 1n << 64n } }, 'outbound.rate'], + [{ inbound: { enabled: true, capacity: 1n, rate: 2n } }, 'inbound.rate'], + [{ outbound: { enabled: false, capacity: 1n, rate: 0n } }, 'outbound'], + [{ inbound: { enabled: 'true', capacity: 1n, rate: 1n } }, 'inbound.enabled'], + ] as const) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).setChainRateLimit({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + remoteChainSelector: SELECTOR, + inbound: { enabled: true, capacity: 100n, rate: 10n }, + outbound: { enabled: true, capacity: 200n, rate: 20n }, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed configuration', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).setChainRateLimit({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + authority: AUTHORITY, + remoteChainSelector: SELECTOR, + inbound: { enabled: true, capacity: 100n, rate: 10n }, + outbound: { enabled: true, capacity: 200n, rate: 20n }, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setChainRateLimit' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/set-chain-rate-limit.ts b/ccip-sdk/src/cct/solana/token-pool/operations/set-chain-rate-limit.ts new file mode 100644 index 00000000..b0d4e752 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/set-chain-rate-limit.ts @@ -0,0 +1,225 @@ +import type { PublicKey } from '@solana/web3.js' +import BN from 'bn.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type PoolProgramRef, + createTokenPoolProgram, + deriveTokenPoolChainConfigPda, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + U64_MAX, + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, + validateBigInt, +} from '../../validate.ts' + +/** + * Configuration for one direction of a token pool rate limiter. + * + * @remarks For a mint with 6 decimals, pass `1_000_000n` to represent one token. + */ +export type RateLimitConfig = + | { + /** Whether this directional rate limit is enforced. */ + enabled: true + /** Maximum token amount in the bucket (`u64`), at least `rate`. */ + capacity: bigint + /** Token amount restored to the bucket per second (`u64`), no greater than `capacity`. */ + rate: bigint + } + | { + /** Whether this directional rate limit is enforced. */ + enabled: false + /** Must be zero when provided; defaults to zero. */ + capacity?: bigint + /** Must be zero when provided; defaults to zero. */ + rate?: bigint + } + +type ParsedRateLimitConfig = { + enabled: boolean + capacity: bigint + rate: bigint +} + +function parseRateLimitConfig( + operation: string, + direction: string, + config: unknown, +): ParsedRateLimitConfig { + if (typeof config !== 'object' || config === null) { + throw new CCTParamsInvalidError(operation, direction, 'must be a rate-limit configuration') + } + + const { + enabled, + capacity: inputCapacity, + rate: inputRate, + } = config as Partial + + if (typeof enabled !== 'boolean') { + throw new CCTParamsInvalidError(operation, `${direction}.enabled`, 'must be a boolean') + } + + const capacity = !enabled && inputCapacity === undefined ? 0n : inputCapacity + const rate = !enabled && inputRate === undefined ? 0n : inputRate + + validateBigInt(operation, `${direction}.capacity`, capacity, 0n, U64_MAX) + validateBigInt(operation, `${direction}.rate`, rate, 0n, U64_MAX) + + if (enabled && rate > capacity) { + throw new CCTParamsInvalidError( + operation, + `${direction}.rate`, + 'must not exceed capacity when enabled', + ) + } + if (!enabled && (capacity !== 0n || rate !== 0n)) { + throw new CCTParamsInvalidError( + operation, + direction, + 'must have zero capacity and rate when disabled', + ) + } + return { enabled, capacity, rate } +} + +/** Parameters shared by Solana token pool rate-limit generation and execution. */ +type SetChainRateLimitParams = PoolProgramRef & { + /** Token mint address managed by the local pool. */ + tokenAddress: string + /** CCIP selector of the remote chain (`u64`). */ + remoteChainSelector: bigint + /** Rate limit for tokens received from the remote chain. Disabled limits default omitted values to zero. */ + inbound: RateLimitConfig + /** Rate limit for tokens sent to the remote chain. Disabled limits default omitted values to zero. */ + outbound: RateLimitConfig + /** Pool owner or rate-limit admin. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedSetChainRateLimitParams = { + tokenAddress: PublicKey + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey + remoteChainSelector: bigint + inbound: ParsedRateLimitConfig + outbound: ParsedRateLimitConfig +} + +/** Parameters for unsigned Solana token pool rate-limit configuration. */ +export type GenerateSetChainRateLimitParams = SolanaGenerateParams + +/** Unsigned Solana token pool rate-limit configuration result. */ +export type GenerateSetChainRateLimitResult = UnsignedSolanaTx + +/** Parameters for executing Solana token pool rate-limit configuration. */ +export type ExecuteSetChainRateLimitParams = SolanaExecuteParams + +/** Result of executing Solana token pool rate-limit configuration. */ +export type ExecuteSetChainRateLimitResult = TransactionResult + +/** + * Sets inbound and outbound rate limits for an initialized remote-chain config. + * + * @remarks `authority` must be the pool owner or rate-limit admin. The remote-chain config must + * already exist. + */ +export class SetChainRateLimit extends SolanaOperation< + SetChainRateLimitParams, + UnsignedSolanaTx, + ParsedSetChainRateLimitParams +> { + readonly name = 'setChainRateLimit' + + /** Parses rate limits and defaults authority to payer without mutating caller params. */ + protected override parse(params: GenerateSetChainRateLimitParams): ParsedSetChainRateLimitParams { + validateBigInt(this.name, 'remoteChainSelector', params.remoteChainSelector, 0n, U64_MAX) + const inbound = parseRateLimitConfig(this.name, 'inbound', params.inbound) + const outbound = parseRateLimitConfig(this.name, 'outbound', params.outbound) + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram: resolvePoolProgram(this.name, params), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + remoteChainSelector: params.remoteChainSelector, + inbound, + outbound, + } + } + + /** Builds the unsigned Solana `setChainRateLimit` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedSetChainRateLimitParams, + ): Promise { + const program = createTokenPoolProgram(chain, opts.poolProgram, opts.payer) + const instruction = await program.methods + .setChainRateLimit( + new BN(opts.remoteChainSelector.toString()), + opts.tokenAddress, + { + ...opts.inbound, + capacity: new BN(opts.inbound.capacity.toString()), + rate: new BN(opts.inbound.rate.toString()), + }, + { + ...opts.outbound, + capacity: new BN(opts.outbound.capacity.toString()), + rate: new BN(opts.outbound.rate.toString()), + }, + ) + .accountsStrict({ + state: deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress), + chainConfig: deriveTokenPoolChainConfigPda( + opts.poolProgram, + opts.remoteChainSelector, + opts.tokenAddress, + ), + authority: opts.authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}, remoteChainSelector = ${opts.remoteChainSelector}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the pool owner or rate-limit admin wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteSetChainRateLimitParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'setChainRateLimit requires authority to be the executing wallet. Use generateUnsignedSetChainRateLimit for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/set-rate-limit-admin.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/set-rate-limit-admin.test.ts new file mode 100644 index 00000000..d62b296b --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/set-rate-limit-admin.test.ts @@ -0,0 +1,146 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveTokenPoolConfigPda, resolveTokenPoolProgram } from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const NEW_RATE_LIMIT_ADMIN = Keypair.generate().publicKey.toBase58() +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function chain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: {}, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(), + connection: { + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(chain()).generateUnsignedSetRateLimitAdmin({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + newRateLimitAdmin: NEW_RATE_LIMIT_ADMIN, + ...opts, + }) +} + +describe('SetRateLimitAdmin (cct/solana)', () => { + describe('generate', () => { + it('builds the set-rate-limit-admin instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('burn-mint') + const decoded = tokenPoolCoder.instruction.decode(instruction!.data) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction!.programId.toBase58(), poolProgram.toBase58()) + assert.deepEqual( + instruction!.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: AUTHORITY, isSigner: true, isWritable: true }, + ], + ) + assert.ok(decoded) + assert.equal(decoded.name, 'setRateLimitAdmin') + const data = decoded.data as { mint: PublicKey; newRateLimitAdmin: PublicKey } + assert.equal(data.mint.toBase58(), TOKEN) + assert.equal(data.newRateLimitAdmin.toBase58(), NEW_RATE_LIMIT_ADMIN) + }) + + it('defaults authority to payer', async () => { + const unsigned = await generate({ authority: undefined }) + + assert.equal(unsigned.instructions[0]!.keys[1]!.pubkey.toBase58(), PAYER) + }) + + it('supports a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await generate({ poolType: undefined, poolProgramAddress }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects invalid public keys', async () => { + for (const [opts, param] of [ + [{ tokenAddress: 'invalid' }, 'tokenAddress'], + [{ newRateLimitAdmin: 'invalid' }, 'newRateLimitAdmin'], + ]) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).setRateLimitAdmin({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + newRateLimitAdmin: NEW_RATE_LIMIT_ADMIN, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed configuration', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).setRateLimitAdmin({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + newRateLimitAdmin: NEW_RATE_LIMIT_ADMIN, + authority: AUTHORITY, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setRateLimitAdmin' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/set-rate-limit-admin.ts b/ccip-sdk/src/cct/solana/token-pool/operations/set-rate-limit-admin.ts new file mode 100644 index 00000000..c9e7ecdf --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/set-rate-limit-admin.ts @@ -0,0 +1,115 @@ +import type { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type PoolProgramRef, + createTokenPoolProgram, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, +} from '../../validate.ts' + +/** Parameters shared by Solana token pool rate-limit admin generation and execution. */ +type SetRateLimitAdminParams = PoolProgramRef & { + /** Token mint address managed by the pool. */ + tokenAddress: string + /** Address authorized to configure the pool's chain rate limits. */ + newRateLimitAdmin: string + /** Pool owner. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedSetRateLimitAdminParams = { + tokenAddress: PublicKey + newRateLimitAdmin: PublicKey + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey +} + +/** Parameters for unsigned Solana token pool rate-limit admin configuration. */ +export type GenerateSetRateLimitAdminParams = SolanaGenerateParams + +/** Unsigned Solana token pool rate-limit admin configuration result. */ +export type GenerateSetRateLimitAdminResult = UnsignedSolanaTx + +/** Parameters for executing Solana token pool rate-limit admin configuration. */ +export type ExecuteSetRateLimitAdminParams = SolanaExecuteParams + +/** Result of executing Solana token pool rate-limit admin configuration. */ +export type ExecuteSetRateLimitAdminResult = TransactionResult + +/** Sets the administrator authorized to configure a Solana token pool's chain rate limits. */ +export class SetRateLimitAdmin extends SolanaOperation< + SetRateLimitAdminParams, + UnsignedSolanaTx, + ParsedSetRateLimitAdminParams +> { + readonly name = 'setRateLimitAdmin' + + /** Parses public keys and defaults authority to payer without mutating caller params. */ + protected override parse(params: GenerateSetRateLimitAdminParams): ParsedSetRateLimitAdminParams { + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + newRateLimitAdmin: parsePublicKey(this.name, 'newRateLimitAdmin', params.newRateLimitAdmin), + poolProgram: resolvePoolProgram(this.name, params), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + } + } + + /** Builds the unsigned Solana `setRateLimitAdmin` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedSetRateLimitAdminParams, + ): Promise { + const program = createTokenPoolProgram(chain, opts.poolProgram, opts.payer) + const instruction = await program.methods + .setRateLimitAdmin(opts.tokenAddress, opts.newRateLimitAdmin) + .accountsStrict({ + state: deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress), + authority: opts.authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the pool owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteSetRateLimitAdminParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'setRateLimitAdmin requires authority to be the executing wallet. Use generateUnsignedSetRateLimitAdmin for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/transfer-ownership.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/transfer-ownership.test.ts new file mode 100644 index 00000000..612381bc --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/transfer-ownership.test.ts @@ -0,0 +1,187 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { BorshAccountsCoder } from '@coral-xyz/anchor' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { tokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { deriveTokenPoolConfigPda, resolveTokenPoolProgram } from '../../programs/token-pool.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const NEW_OWNER = Keypair.generate().publicKey.toBase58() +const OWNER = Keypair.generate().publicKey +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stateData(owner = OWNER): Buffer { + const key = PublicKey.default.toBuffer() + return Buffer.concat([ + BorshAccountsCoder.accountDiscriminator('State'), + Buffer.from([1]), + key, + new PublicKey(TOKEN).toBuffer(), + Buffer.from([6]), + key, + key, + owner.toBuffer(), + key, + key, + key, + key, + key, + Buffer.from([0, 0]), + Buffer.alloc(4), + key, + ]) +} + +function chain(owner = OWNER): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async () => ({ owner: PublicKey.default, data: stateData(owner) }), + }, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(), + connection: { + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + getAccountInfo: async () => ({ owner: PublicKey.default, data: stateData() }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(chain()).generateUnsignedTransferOwnership({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + newOwner: NEW_OWNER, + ...opts, + }) +} + +describe('TransferOwnership (cct/solana)', () => { + describe('generate', () => { + it('builds the ownership-transfer instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('burn-mint') + const decoded = tokenPoolCoder.instruction.decode(instruction!.data) + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction!.programId.toBase58(), poolProgram.toBase58()) + assert.deepEqual( + instruction!.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { + pubkey: deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: TOKEN, isSigner: false, isWritable: false }, + { pubkey: AUTHORITY, isSigner: true, isWritable: false }, + ], + ) + assert.ok(decoded) + assert.equal(decoded.name, 'transferOwnership') + assert.equal( + (decoded.data as { proposedOwner: PublicKey }).proposedOwner.toBase58(), + NEW_OWNER, + ) + }) + + it('defaults authority to payer', async () => { + const unsigned = await generate({ authority: undefined }) + + assert.equal(unsigned.instructions[0]!.keys[2]!.pubkey.toBase58(), PAYER) + }) + + it('supports a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await generate({ poolType: undefined, poolProgramAddress }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects invalid public keys', async () => { + for (const [opts, param] of [ + [{ tokenAddress: 'invalid' }, 'tokenAddress'], + [{ newOwner: 'invalid' }, 'newOwner'], + [{ newOwner: PublicKey.default.toBase58() }, 'newOwner'], + ]) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + + it('rejects the current pool owner', async () => { + await assert.rejects( + () => generate({ newOwner: OWNER.toBase58() }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'transferOwnership' && + err.context.param === 'newOwner' && + err.message.includes('must not be the current pool owner'), + ) + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).transferOwnership({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + newOwner: NEW_OWNER, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed transfer', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).transferOwnership({ + tokenAddress: TOKEN, + poolType: 'burn-mint', + newOwner: NEW_OWNER, + authority: AUTHORITY, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'transferOwnership' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/transfer-ownership.ts b/ccip-sdk/src/cct/solana/token-pool/operations/transfer-ownership.ts new file mode 100644 index 00000000..3273b78d --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/transfer-ownership.ts @@ -0,0 +1,136 @@ +import { PublicKey } from '@solana/web3.js' + +import { GetTokenPoolState } from './get-token-pool-state.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type PoolProgramRef, + createTokenPoolProgram, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, +} from '../../validate.ts' + +/** Parameters shared by Solana token pool ownership-transfer generation and execution. */ +type TransferOwnershipParams = PoolProgramRef & { + /** Token mint address managed by the pool. */ + tokenAddress: string + /** Address proposed as the next pool owner. It must accept ownership separately. */ + newOwner: string + /** Current pool owner. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedTransferOwnershipParams = { + tokenAddress: PublicKey + newOwner: PublicKey + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey +} + +/** Parameters for unsigned Solana token pool ownership transfer. */ +export type GenerateTransferOwnershipParams = SolanaGenerateParams + +/** Unsigned Solana token pool ownership transfer result. */ +export type GenerateTransferOwnershipResult = UnsignedSolanaTx + +/** Parameters for executing Solana token pool ownership transfer. */ +export type ExecuteTransferOwnershipParams = SolanaExecuteParams + +/** Result of executing Solana token pool ownership transfer. */ +export type ExecuteTransferOwnershipResult = TransactionResult + +/** Proposes a new owner for a Solana token pool. The proposed owner must accept separately. */ +export class TransferOwnership extends SolanaOperation< + TransferOwnershipParams, + UnsignedSolanaTx, + ParsedTransferOwnershipParams +> { + readonly name = 'transferOwnership' + + /** Parses public keys and defaults authority to payer without mutating caller params. */ + protected override parse(params: GenerateTransferOwnershipParams): ParsedTransferOwnershipParams { + const payer = parsePublicKey(this.name, 'payer', params.payer) + const newOwner = parsePublicKey(this.name, 'newOwner', params.newOwner) + if (newOwner.equals(PublicKey.default)) { + throw new CCTParamsInvalidError( + this.name, + 'newOwner', + 'must not be the default public key or zero address', + ) + } + + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + newOwner, + poolProgram: resolvePoolProgram(this.name, params), + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + } + } + + /** Reads the pool state to reject self-transfer, then builds the unsigned Solana `transferOwnership` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedTransferOwnershipParams, + ): Promise { + const { config } = await new GetTokenPoolState().query(chain, { + tokenAddress: opts.tokenAddress.toBase58(), + poolProgramAddress: opts.poolProgram.toBase58(), + }) + + if (opts.newOwner.equals(new PublicKey(config.owner))) { + throw new CCTParamsInvalidError(this.name, 'newOwner', 'must not be the current pool owner') + } + + const program = createTokenPoolProgram(chain, opts.poolProgram, opts.payer) + const instruction = await program.methods + .transferOwnership(opts.newOwner) + .accountsStrict({ + state: deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress), + mint: opts.tokenAddress, + authority: opts.authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the current pool owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteTransferOwnershipParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'transferOwnership requires authority to be the executing wallet. Use generateUnsignedTransferOwnership for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/token/operations/create-token-account.test.ts b/ccip-sdk/src/cct/solana/token/operations/create-token-account.test.ts new file mode 100644 index 00000000..c36517b8 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/create-token-account.test.ts @@ -0,0 +1,104 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, +} from '@solana/spl-token' +import { type PublicKey, Keypair } from '@solana/web3.js' + +import { + CCIPTokenMintInvalidError, + CCIPTokenMintNotFoundError, + CCIPWalletInvalidError, +} from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { SolanaTokenManager } from '../../index.ts' + +const PAYER = Keypair.generate().publicKey.toBase58() +const MINT = Keypair.generate().publicKey +const OWNER = Keypair.generate().publicKey + +function stubChain(mintOwner: PublicKey | null = TOKEN_2022_PROGRAM_ID): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async () => (mintOwner ? { owner: mintOwner } : null), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}, mintOwner?: PublicKey | null) { + return SolanaTokenManager.fromChain(stubChain(mintOwner)).generateUnsignedCreateTokenAccount({ + payer: PAYER, + tokenAddress: MINT.toBase58(), + ownerAddress: OWNER.toBase58(), + ...opts, + }) +} + +describe('CreateTokenAccount (cct/solana)', () => { + describe('generate', () => { + it('builds an idempotent ATA create instruction for any owner', async () => { + const unsigned = await generate() + const [ix] = unsigned.instructions + const ata = getAssociatedTokenAddressSync(MINT, OWNER, true, TOKEN_2022_PROGRAM_ID) + + assert.ok(ix) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.tokenAccountAddress, ata.toBase58()) + assert.equal(ix.programId.toBase58(), ASSOCIATED_TOKEN_PROGRAM_ID.toBase58()) + assert.equal(ix.data.length, 1) + assert.equal(ix.data[0], 1) // CreateIdempotent + assert.equal(ix.keys[0]!.pubkey.toBase58(), PAYER) + assert.equal(ix.keys[1]!.pubkey.toBase58(), ata.toBase58()) + assert.equal(ix.keys[2]!.pubkey.toBase58(), OWNER.toBase58()) + assert.equal(ix.keys[3]!.pubkey.toBase58(), MINT.toBase58()) + assert.equal(ix.keys.at(-1)!.pubkey.toBase58(), TOKEN_2022_PROGRAM_ID.toBase58()) + }) + + it('builds for legacy SPL Token mints', async () => { + const unsigned = await generate({}, TOKEN_PROGRAM_ID) + const ata = getAssociatedTokenAddressSync(MINT, OWNER, true, TOKEN_PROGRAM_ID) + + assert.equal(unsigned.tokenAccountAddress, ata.toBase58()) + assert.equal( + unsigned.instructions[0]!.keys.at(-1)!.pubkey.toBase58(), + TOKEN_PROGRAM_ID.toBase58(), + ) + }) + }) + + describe('validation', () => { + it('rejects a missing mint', async () => { + await assert.rejects( + () => generate({}, null), + (err: unknown) => err instanceof CCIPTokenMintNotFoundError, + ) + }) + + it('rejects non-token mint accounts', async () => { + await assert.rejects( + () => generate({}, Keypair.generate().publicKey), + (err: unknown) => err instanceof CCIPTokenMintInvalidError, + ) + }) + }) + + describe('execute', () => { + it('rejects an invalid wallet before generating instructions', async () => { + await assert.rejects( + SolanaTokenManager.fromChain(stubChain()).createTokenAccount({ + tokenAddress: MINT.toBase58(), + ownerAddress: OWNER.toBase58(), + wallet: {}, + }), + CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/create-token-account.ts b/ccip-sdk/src/cct/solana/token/operations/create-token-account.ts new file mode 100644 index 00000000..bb0941dc --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/create-token-account.ts @@ -0,0 +1,102 @@ +import { createAssociatedTokenAccountIdempotentInstruction } from '@solana/spl-token' +import type { PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { resolveATA } from '../../../../solana/utils.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { submit } from '../../submit.ts' +import { parsePublicKey } from '../../validate.ts' + +/** Parameters for deriving and creating a Solana associated token account. */ +type CreateTokenAccountParams = { + /** SPL token mint address for the associated token account. */ + tokenAddress: string + /** Wallet or PDA owner address for the associated token account. */ + ownerAddress: string +} + +/** Parameters for unsigned Solana associated token account creation. */ +export type GenerateCreateTokenAccountParams = SolanaGenerateParams + +type ParsedCreateTokenAccountParams = { + payer: PublicKey + tokenAddress: PublicKey + ownerAddress: PublicKey +} + +/** Unsigned associated token account creation tx plus the derived token account address. */ +export type GenerateCreateTokenAccountResult = UnsignedSolanaTx & { tokenAccountAddress: string } + +/** Parameters for executing Solana associated token account creation. */ +export type ExecuteCreateTokenAccountParams = SolanaExecuteParams + +/** Result of executing Solana associated token account creation. */ +export type ExecuteCreateTokenAccountResult = TransactionResult & { tokenAccountAddress: string } + +/** Creates an Associated Token Account for any wallet or PDA owner. */ +export class CreateTokenAccount extends SolanaOperation< + CreateTokenAccountParams, + GenerateCreateTokenAccountResult, + ParsedCreateTokenAccountParams +> { + readonly name = 'createTokenAccount' + + /** Parses create-token-account parameters. */ + protected override parse( + params: GenerateCreateTokenAccountParams, + ): ParsedCreateTokenAccountParams { + return { + payer: parsePublicKey(this.name, 'payer', params.payer), + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + ownerAddress: parsePublicKey(this.name, 'ownerAddress', params.ownerAddress), + } + } + + /** Builds an unsigned idempotent associated token account creation transaction. */ + protected async buildUnsigned( + chain: SolanaChain, + params: ParsedCreateTokenAccountParams, + ): Promise { + const { payer, tokenAddress: mint, ownerAddress: owner } = params + const { ata: tokenAccount, tokenProgram } = await resolveATA(chain.connection, mint, owner) + + chain.logger.debug( + `${this.name}: mint = ${mint.toBase58()}, owner = ${owner.toBase58()}, tokenAccount = ${tokenAccount.toBase58()}, tokenProgram = ${tokenProgram.toBase58()}`, + ) + + return { + family: ChainFamily.Solana, + instructions: [ + createAssociatedTokenAccountIdempotentInstruction( + payer, + tokenAccount, + owner, + mint, + tokenProgram, + ), + ], + mainIndex: 0, + tokenAccountAddress: tokenAccount.toBase58(), + } + } + + /** Generate, sign, simulate, send, confirm, and return the derived token account address. */ + override async execute( + chain: SolanaChain, + params: ExecuteCreateTokenAccountParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + const tx = await this.buildUnsigned(chain, parsed) + const hash = await submit(chain, wallet, tx, this.name, computeUnits) + + return { ...hash, tokenAccountAddress: tx.tokenAccountAddress } + } +} diff --git a/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts b/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts new file mode 100644 index 00000000..b891c866 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts @@ -0,0 +1,147 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' + +const BLOCKHASH = PublicKey.default.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const METAPLEX_PROGRAM = 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s' + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + rpcEndpoint: 'http://localhost:8899', + getAccountInfo: () => assert.fail('should not RPC before validation'), + getMinimumBalanceForRentExemption: async () => 123, + getLatestBlockhash: async () => ({ blockhash: BLOCKHASH, lastValidBlockHeight: 0 }), + }, + } as unknown as SolanaChain +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedDeployToken({ + decimals: 9, + withMetaplex: false, + payer: PAYER, + ...opts, + }) +} + +describe('DeployToken (cct/solana)', () => { + describe('generate', () => { + it('builds unsigned SPL mint create instructions', async () => { + const unsigned = await generate() + const [createAccountIx, initializeMintIx] = unsigned.instructions + + assert.ok(createAccountIx) + assert.ok(initializeMintIx) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.match(unsigned.tokenAddress, /^[1-9A-HJ-NP-Za-km-z]+$/) + assert.equal('seed' in unsigned, false) + assert.equal(unsigned.metadataAddress, undefined) + assert.equal(unsigned.instructions.length, 2) + assert.equal(createAccountIx.programId.toBase58(), SystemProgram.programId.toBase58()) + assert.equal(initializeMintIx.programId.toBase58(), TOKEN_PROGRAM_ID.toBase58()) + assert.equal(initializeMintIx.data[0], 20) // InitializeMint2, not legacy InitializeMint + }) + + it('uses caller seed for reproducible mint address', async () => { + const a = await generate({ seed: 'mint_seed' }) + const b = await generate({ seed: 'mint_seed' }) + + assert.equal(a.tokenAddress, b.tokenAddress) + }) + + it('adds Metaplex metadata when requested', async () => { + const unsigned = await generate({ + withMetaplex: true, + name: 'My Token', + symbol: 'MTK', + }) + + assert.equal(unsigned.instructions.length, 3) + assert.match(unsigned.metadataAddress!, /^[1-9A-HJ-NP-Za-km-z]+$/) + assert.equal(unsigned.instructions[2]!.programId.toBase58(), METAPLEX_PROGRAM) + assert.equal(unsigned.instructions[2]!.data[0], 42) // createV1 + }) + + it('uses Token-2022 program for mint and metadata', async () => { + const unsigned = await generate({ + tokenProgram: 'token-2022', + withMetaplex: true, + name: 'My Token', + symbol: 'MTK', + }) + + assert.equal(unsigned.instructions[1]!.programId.toBase58(), TOKEN_2022_PROGRAM_ID.toBase58()) + assert.ok( + unsigned.instructions[2]!.keys.some( + (key) => key.pubkey.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58(), + ), + ) + }) + + it('adds ATA creation and mintTo instructions for preMint', async () => { + const unsigned = await generate({ + preMint: 100n, + preMintRecipient: Keypair.generate().publicKey.toBase58(), + }) + + assert.equal(unsigned.instructions.length, 4) + assert.equal(unsigned.instructions[3]!.data[0], 7) // MintTo + }) + }) + + describe('execute', () => { + it('rejects execute when preMint needs a non-wallet mintAuthority signer', async () => { + const wallet = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, + } + + await assert.rejects( + () => + SolanaTokenManager.fromChain(stubChain()).deployToken({ + wallet, + decimals: 9, + tokenProgram: 'spl-token', + withMetaplex: false, + mintAuthority: Keypair.generate().publicKey.toBase58(), + preMint: 100n, + preMintRecipient: Keypair.generate().publicKey.toBase58(), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'mintAuthority', + ) + }) + }) + + describe('validation', () => { + it('rejects seeds over 32 UTF-8 bytes', async () => { + await assert.rejects( + () => generate({ seed: '🚀'.repeat(9) }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'seed', + ) + }) + + it('validates Metaplex name and symbol by UTF-8 byte length', async () => { + await assert.rejects( + () => + generate({ + withMetaplex: true, + name: 'Valid', + symbol: '🚀🚀🚀', + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'symbol', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts new file mode 100644 index 00000000..64501626 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts @@ -0,0 +1,378 @@ +import { + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + createAssociatedTokenAccountIdempotentInstruction, + createInitializeMint2Instruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + getMintLen, +} from '@solana/spl-token' +import { type TransactionInstruction, PublicKey, SystemProgram } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { submit } from '../../submit.ts' +import { validateOptionalPublicKey, validatePublicKey } from '../../validate.ts' + +type BaseDeployTokenParams = { + /** Mint decimals. Must be an integer between 0 and 255. */ + decimals: number + /** Token program that owns the mint: classic SPL Token or Token-2022. Defaults to spl-token. */ + tokenProgram?: 'spl-token' | 'token-2022' + /** Mint authority. Defaults to payer. */ + mintAuthority?: string + /** Freeze authority. Defaults to payer; set null to disable freezing. */ + freezeAuthority?: string | null + /** Initial supply in base units. Requires preMintRecipient. */ + preMint?: bigint + /** Recipient owner for the initial supply ATA. */ + preMintRecipient?: string + /** Seed for deterministic mint address derivation. Defaults to a random seed. Max 32 UTF-8 bytes. */ + seed?: string +} + +/** + * Parameters for creating a Solana SPL mint. + * + * Set `withMetaplex: true` to create Metaplex metadata; `name` and `symbol` are required; + */ +type DeployTokenParams = BaseDeployTokenParams & + ( + | { withMetaplex: false } + | { + withMetaplex: true + /** Token display name for Metaplex metadata. Max 32 UTF-8 bytes. */ + name: string + /** Token symbol for Metaplex metadata. Max 10 UTF-8 bytes. */ + symbol: string + /** Metadata URI for Metaplex metadata JSON. Optional; defaults to an empty string when omitted. */ + uri?: string | undefined + } + ) + +/** Parameters for unsigned Solana token deploy generation. */ +export type GenerateDeployTokenParams = SolanaGenerateParams + +/** Unsigned token deploy transaction plus the created mint address. */ +export type GenerateDeployTokenResult = UnsignedSolanaTx & { + tokenAddress: string + metadataAddress?: string +} + +/** Parameters for executing Solana token deploy. */ +export type ExecuteDeployTokenParams = SolanaExecuteParams + +/** Result of executing Solana token deploy. */ +export type ExecuteDeployTokenResult = TransactionResult & { + tokenAddress: string + metadataAddress?: string +} + +const METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s') + +function utf8ByteLength(value: string): number { + return new TextEncoder().encode(value).length +} + +function deriveMetadataAddress(mint: PublicKey): string { + return PublicKey.findProgramAddressSync( + [Buffer.from('metadata'), METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()], + METADATA_PROGRAM_ID, + )[0].toBase58() +} + +async function loadMetaplex() { + const [metadata, umi, bundleDefaults, web3] = await Promise.all([ + import('@metaplex-foundation/mpl-token-metadata'), + import('@metaplex-foundation/umi'), + import('@metaplex-foundation/umi-bundle-defaults'), + import('@metaplex-foundation/umi-web3js-adapters'), + ]) + + return { + TokenStandard: metadata.TokenStandard, + createNoopSigner: umi.createNoopSigner, + createUmi: bundleDefaults.createUmi, + createV1: metadata.createV1, + mplTokenMetadata: metadata.mplTokenMetadata, + percentAmount: umi.percentAmount, + publicKey: umi.publicKey, + signerIdentity: umi.signerIdentity, + toWeb3JsInstruction: web3.toWeb3JsInstruction, + } +} + +async function createMetadataInstructions( + chain: SolanaChain, + mint: PublicKey, + payer: PublicKey, + tokenProgram: PublicKey, + decimals: number, + mintAuthority: PublicKey, + params: { name: string; symbol: string; uri: string }, +): Promise { + const metaplex = await loadMetaplex() + const payerSigner = metaplex.createNoopSigner(metaplex.publicKey(payer.toBase58())) + const mintAuthoritySigner = metaplex.createNoopSigner( + metaplex.publicKey(mintAuthority.toBase58()), + ) + const metadataUmi = metaplex + .createUmi(chain.connection) + .use(metaplex.mplTokenMetadata()) + .use(metaplex.signerIdentity(payerSigner)) + + return metaplex + .createV1(metadataUmi, { + mint: metaplex.publicKey(mint.toBase58()), + authority: mintAuthoritySigner, + payer: payerSigner, + updateAuthority: mintAuthoritySigner, + splTokenProgram: metaplex.publicKey(tokenProgram.toBase58()), + name: params.name, + symbol: params.symbol, + uri: params.uri, + sellerFeeBasisPoints: metaplex.percentAmount(0), + decimals, + tokenStandard: metaplex.TokenStandard.Fungible, + }) + .getInstructions() + .map(metaplex.toWeb3JsInstruction) +} + +type DeployTokenConfig = { + payer: PublicKey + mintAuthority: PublicKey + freezeAuthority: PublicKey | null + tokenProgram: PublicKey + seed: string +} + +type ParsedDeployTokenParams = GenerateDeployTokenParams & { config: DeployTokenConfig } + +function resolveDeployTokenConfig(params: GenerateDeployTokenParams): DeployTokenConfig { + const payer = new PublicKey(params.payer) + return { + payer, + mintAuthority: new PublicKey(params.mintAuthority ?? params.payer), + freezeAuthority: + params.freezeAuthority === null + ? null + : new PublicKey(params.freezeAuthority ?? params.payer), + tokenProgram: params.tokenProgram === 'token-2022' ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID, + seed: params.seed ?? `mint_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + } +} + +function createMintInstructions( + mint: PublicKey, + lamports: number, + decimals: number, + config: DeployTokenConfig, +): TransactionInstruction[] { + return [ + SystemProgram.createAccountWithSeed({ + fromPubkey: config.payer, + newAccountPubkey: mint, + basePubkey: config.payer, + seed: config.seed, + lamports, + space: getMintLen([]), + programId: config.tokenProgram, + }), + createInitializeMint2Instruction( + mint, + decimals, + config.mintAuthority, + config.freezeAuthority, + config.tokenProgram, + ), + ] +} + +function createPreMintInstructions( + mint: PublicKey, + params: GenerateDeployTokenParams, + config: DeployTokenConfig, +): TransactionInstruction[] { + if (params.preMint === undefined) return [] + + const recipient = new PublicKey(params.preMintRecipient!) + const ata = getAssociatedTokenAddressSync(mint, recipient, false, config.tokenProgram) + return [ + createAssociatedTokenAccountIdempotentInstruction( + config.payer, + ata, + recipient, + mint, + config.tokenProgram, + ), + createMintToInstruction( + mint, + ata, + config.mintAuthority, + params.preMint, + [], + config.tokenProgram, + ), + ] +} + +function getExternalMintAuthoritySigner( + params: DeployTokenParams, + payer: string, +): string | undefined { + const mintAuthority = params.mintAuthority ?? payer + return (params.withMetaplex || params.preMint !== undefined) && mintAuthority !== payer + ? mintAuthority + : undefined +} + +function validateBaseParams(operation: string, params: GenerateDeployTokenParams): void { + validatePublicKey(operation, 'payer', params.payer) + if (!Number.isInteger(params.decimals) || params.decimals < 0 || params.decimals > 255) { + throw new CCTParamsInvalidError(operation, 'decimals', 'must be an integer between 0 and 255') + } + if (params.tokenProgram && !['spl-token', 'token-2022'].includes(params.tokenProgram)) { + throw new CCTParamsInvalidError(operation, 'tokenProgram', 'must be spl-token or token-2022') + } + if (typeof params.withMetaplex !== 'boolean') { + throw new CCTParamsInvalidError(operation, 'withMetaplex', 'must be a boolean') + } + if (params.seed !== undefined && (!params.seed || utf8ByteLength(params.seed) > 32)) { + throw new CCTParamsInvalidError(operation, 'seed', 'must be non-empty and <= 32 UTF-8 bytes') + } + validateOptionalPublicKey(operation, 'mintAuthority', params.mintAuthority) + if (params.freezeAuthority !== undefined && params.freezeAuthority !== null) { + validatePublicKey(operation, 'freezeAuthority', params.freezeAuthority) + } +} + +function validatePreMintParams(operation: string, params: GenerateDeployTokenParams): void { + if ( + params.preMint !== undefined && + (typeof params.preMint !== 'bigint' || params.preMint <= 0n) + ) { + throw new CCTParamsInvalidError(operation, 'preMint', 'must be a positive bigint') + } + if (params.preMint !== undefined && !params.preMintRecipient) { + throw new CCTParamsInvalidError( + operation, + 'preMintRecipient', + 'is required when preMint is set', + ) + } + validateOptionalPublicKey(operation, 'preMintRecipient', params.preMintRecipient) +} + +function validateMetaplexParams(operation: string, params: GenerateDeployTokenParams): void { + if (!params.withMetaplex) return + if (!params.name || utf8ByteLength(params.name) > 32) { + throw new CCTParamsInvalidError( + operation, + 'name', + 'is required and must be <= 32 UTF-8 bytes when withMetaplex is true', + ) + } + if (!params.symbol || utf8ByteLength(params.symbol) > 10) { + throw new CCTParamsInvalidError( + operation, + 'symbol', + 'is required and must be <= 10 UTF-8 bytes when withMetaplex is true', + ) + } + if (params.uri !== undefined && typeof params.uri !== 'string') { + throw new CCTParamsInvalidError(operation, 'uri', 'must be a string when provided') + } +} + +/** Creates a Solana SPL mint, optionally with Metaplex metadata and initial supply. */ +export class DeployToken extends SolanaOperation< + DeployTokenParams, + GenerateDeployTokenResult, + ParsedDeployTokenParams +> { + readonly name = 'deployToken' + + /** Parses mint and metadata params before any RPC. */ + protected override parse(params: GenerateDeployTokenParams): ParsedDeployTokenParams { + validateBaseParams(this.name, params) + validatePreMintParams(this.name, params) + validateMetaplexParams(this.name, params) + return { ...params, config: resolveDeployTokenConfig(params) } + } + + /** Builds the unsigned Solana mint creation instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + params: ParsedDeployTokenParams, + ): Promise { + const { config } = params + const mint = await PublicKey.createWithSeed(config.payer, config.seed, config.tokenProgram) + const lamports = await chain.connection.getMinimumBalanceForRentExemption(getMintLen([])) + const instructions = createMintInstructions(mint, lamports, params.decimals, config) + + const metadataAddress = params.withMetaplex ? deriveMetadataAddress(mint) : undefined + if (params.withMetaplex) + instructions.push( + ...(await createMetadataInstructions( + chain, + mint, + config.payer, + config.tokenProgram, + params.decimals, + config.mintAuthority, + { + name: params.name, + symbol: params.symbol, + uri: params.uri ?? '', + }, + )), + ) + + instructions.push(...createPreMintInstructions(mint, params, config)) + + chain.logger.debug( + `${this.name}: mint = ${mint.toBase58()}, tokenProgram = ${config.tokenProgram.toBase58()}`, + ) + return { + family: ChainFamily.Solana, + instructions, + mainIndex: 0, + tokenAddress: mint.toBase58(), + ...(metadataAddress ? { metadataAddress } : {}), + } + } + + /** Generate, sign, simulate, send, confirm, and return the created mint address. */ + override async execute( + chain: SolanaChain, + params: ExecuteDeployTokenParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + const externalSigner = getExternalMintAuthoritySigner(parsed, parsed.payer) + + if (externalSigner) { + throw new CCTParamsInvalidError( + this.name, + 'mintAuthority', + `requires additional signer: ${externalSigner}. Use generateUnsignedDeployToken and sign externally.`, + ) + } + + const tx = await this.buildUnsigned(chain, parsed) + const hash = await submit(chain, wallet, tx, this.name, computeUnits) + return { + ...hash, + tokenAddress: tx.tokenAddress, + ...(tx.metadataAddress ? { metadataAddress: tx.metadataAddress } : {}), + } + } +} diff --git a/ccip-sdk/src/cct/solana/token/operations/index.ts b/ccip-sdk/src/cct/solana/token/operations/index.ts new file mode 100644 index 00000000..e397c117 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/index.ts @@ -0,0 +1,2 @@ +export * from './create-token-account.ts' +export * from './deploy-token.ts' diff --git a/ccip-sdk/src/cct/solana/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts new file mode 100644 index 00000000..9cdb4c8f --- /dev/null +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -0,0 +1,206 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey } from '@solana/web3.js' + +import { + parseHexBytes, + parseNonEmptyHexBytes, + parsePublicKey, + resolvePoolProgram, + validateBigInt, + validateInteger, + validateNonEmptyString, + validateOptionalPublicKey, + validatePoolType, + validatePublicKey, + validatePublicKeys, + validateWritableIndexes, +} from './validate.ts' +import { CCTParamsInvalidError } from '../errors.ts' +import { type PoolProgramRef, TOKEN_POOL_PROGRAMS } from './programs/token-pool.ts' + +describe('Validate (cct/solana)', () => { + it('parses valid public keys', () => { + const key = parsePublicKey('op', 'payer', PublicKey.default.toBase58()) + assert.ok(key.equals(PublicKey.default)) + }) + + it('parses hex bytes with an optional maximum size', () => { + assert.deepEqual(parseHexBytes('op', 'address', '0x01ab', 2), Buffer.from('01ab', 'hex')) + assert.deepEqual(parseHexBytes('op', 'address', ''), Buffer.alloc(0)) + assert.throws( + () => parseHexBytes('op', 'address', '0x123', 2), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.reason === 'must be a hex string of at most 2 bytes', + ) + assert.throws(() => parseHexBytes('op', 'address', null), CCTParamsInvalidError) + }) + + it('rejects empty hex bytes when required', () => { + assert.deepEqual(parseNonEmptyHexBytes('op', 'address', '0x01'), Buffer.from([1])) + assert.throws( + () => parseNonEmptyHexBytes('op', 'address', ''), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.reason === 'must not be empty', + ) + }) + + it('accepts valid public keys', () => { + assert.doesNotThrow(() => validatePublicKey('op', 'payer', PublicKey.default.toBase58())) + }) + + it('accepts omitted and valid optional public keys', () => { + assert.doesNotThrow(() => validateOptionalPublicKey('op', 'authority', undefined)) + assert.doesNotThrow(() => + validateOptionalPublicKey('op', 'authority', PublicKey.default.toBase58()), + ) + }) + + it('rejects invalid optional public keys', () => { + for (const value of [null, '']) { + assert.throws( + () => validateOptionalPublicKey('op', 'authority', value), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'authority', + ) + } + }) + + it('rejects non-string public keys', () => { + assert.throws( + () => validatePublicKey('op', 'payer', 123), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'payer', + ) + }) + + it('rejects invalid public key strings', () => { + assert.throws( + () => validatePublicKey('op', 'payer', 'nope'), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'payer', + ) + }) + + it('validates public key arrays', () => { + assert.doesNotThrow(() => validatePublicKeys('op', 'signers', [])) + assert.doesNotThrow(() => validatePublicKeys('op', 'signers', [PublicKey.default.toBase58()])) + assert.throws( + () => validatePublicKeys('op', 'signers', ['nope']), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'signers[0]', + ) + assert.throws( + () => validatePublicKeys('op', 'signers', 'nope'), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'signers', + ) + }) + + it('validates non-empty strings', () => { + assert.doesNotThrow(() => validateNonEmptyString('op', 'seed', 'abc')) + assert.throws( + () => validateNonEmptyString('op', 'seed', ' '), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'seed', + ) + }) + + it('validates pool types', () => { + assert.doesNotThrow(() => validatePoolType('op', 'poolType', 'burn-mint')) + assert.doesNotThrow(() => validatePoolType('op', 'poolType', 'lock-release')) + assert.throws( + () => validatePoolType('op', 'poolType', 'nope'), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'poolType', + ) + }) + + it('resolves pool programs', () => { + assert.equal( + resolvePoolProgram('op', { poolType: 'burn-mint' }).toBase58(), + TOKEN_POOL_PROGRAMS['burn-mint'], + ) + assert.ok( + resolvePoolProgram('op', { poolProgramAddress: PublicKey.default.toBase58() }).equals( + PublicKey.default, + ), + ) + + const invalidRefs: unknown[] = [ + {}, + { poolType: 'burn-mint', poolProgramAddress: PublicKey.default.toBase58() }, + { poolType: 'nope' }, + { poolProgramAddress: 'nope' }, + ] + for (const params of invalidRefs) { + assert.throws(() => resolvePoolProgram('op', params as PoolProgramRef), CCTParamsInvalidError) + } + }) + + it('resolves pool references with the other key explicitly undefined', () => { + // Value semantics: an explicitly-set `undefined` key must not count as provided. + const custom = PublicKey.default.toBase58() + + assert.equal( + resolvePoolProgram('op', { poolProgramAddress: custom, poolType: undefined }).toBase58(), + custom, + ) + assert.equal( + resolvePoolProgram('op', { + poolType: 'burn-mint', + poolProgramAddress: undefined, + }).toBase58(), + TOKEN_POOL_PROGRAMS['burn-mint'], + ) + }) + + it('validates integers', () => { + assert.doesNotThrow(() => validateInteger('op', 'threshold', 1)) + assert.doesNotThrow(() => validateInteger('op', 'decimals', 255, 0, 255)) + assert.throws( + () => validateInteger('op', 'decimals', 256, 0, 255), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'decimals', + ) + }) + + it('validates bigint bounds with useful errors', () => { + assert.doesNotThrow(() => validateBigInt('op', 'selector', 0n, 0n)) + assert.throws( + () => validateBigInt('op', 'selector', -1n, 0n), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.reason === 'must be a bigint >= 0', + ) + assert.throws( + () => validateBigInt('op', 'selector', 2n, undefined, 1n), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.reason === 'must be a bigint <= 1', + ) + }) + + it('accepts omitted and valid writable indexes', () => { + assert.doesNotThrow(() => validateWritableIndexes('op', 'writableIndexes', undefined)) + assert.doesNotThrow(() => validateWritableIndexes('op', 'writableIndexes', [0, 3, 255])) + }) + + it('rejects empty writable indexes', () => { + assert.throws( + () => validateWritableIndexes('op', 'writableIndexes', []), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'writableIndexes', + ) + }) + + it('rejects writable indexes outside byte range', () => { + assert.throws( + () => validateWritableIndexes('op', 'writableIndexes', [256]), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'writableIndexes[0]', + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts new file mode 100644 index 00000000..f51836d7 --- /dev/null +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -0,0 +1,248 @@ +import { Buffer } from 'buffer' + +import { PublicKey } from '@solana/web3.js' + +import { CCIPAddressInvalidError } from '../../errors/index.ts' +import { ChainFamily } from '../../networks.ts' +import { CCTParamsInvalidError } from '../errors.ts' +import { + type PoolProgramRef, + type TokenPoolType, + TOKEN_POOL_PROGRAMS, + resolveTokenPoolProgram, +} from './programs/token-pool.ts' + +/** Largest value representable by an unsigned 64-bit integer. */ +export const U64_MAX = 0xffff_ffff_ffff_ffffn + +/** + * Parses `value` as a Solana public key. + * @throws CCTParamsInvalidError if `value` is not a valid Solana public key string. + */ +export function parsePublicKey(operation: string, param: string, value: unknown): PublicKey { + if (typeof value !== 'string') { + throw new CCTParamsInvalidError( + operation, + param, + `must be a valid Solana public key, got "${String(value)}"`, + ) + } + + try { + return new PublicKey(value) + } catch { + throw new CCTParamsInvalidError( + operation, + param, + `must be a valid Solana public key, got "${String(value)}"`, + { + cause: new CCIPAddressInvalidError(value, ChainFamily.Solana), + }, + ) + } +} + +/** + * Asserts `value` is a valid Solana public key string. + * @throws CCTParamsInvalidError if `value` is not a valid Solana public key string. + */ +export function validatePublicKey( + operation: string, + param: string, + value: unknown, +): asserts value is string { + parsePublicKey(operation, param, value) +} + +/** + * Asserts `value` is a valid Solana public key string, or is absent. + * Only `undefined` counts as absent; `null` and `''` are treated as provided and rejected. + * @throws {@link CCTParamsInvalidError} if a non-`undefined` `value` is not a valid public key string. + */ +export function validateOptionalPublicKey( + operation: string, + param: string, + value: unknown, +): asserts value is string | undefined { + if (value !== undefined) validatePublicKey(operation, param, value) +} + +/** + * Asserts `values` is an array of valid Solana public key strings. + * @throws CCTParamsInvalidError if `values` is not an array or any item is invalid. + */ +export function validatePublicKeys(operation: string, param: string, values: unknown): void { + if (!Array.isArray(values)) throw new CCTParamsInvalidError(operation, param, 'must be an array') + for (const [i, value] of values.entries()) validatePublicKey(operation, `${param}[${i}]`, value) +} + +/** + * Asserts `value` is a non-empty string. + * @throws CCTParamsInvalidError if `value` is not a non-empty string. + */ +export function validateNonEmptyString(operation: string, param: string, value: unknown): void { + if (typeof value === 'string' && value.trim().length > 0) return + throw new CCTParamsInvalidError(operation, param, 'must be a non-empty string') +} + +/** + * Asserts an authority matches the executing wallet. + * @throws CCTParamsInvalidError if authority does not match wallet. + */ +export function validateAuthorityMatchesWallet( + operation: string, + authority: PublicKey, + wallet: PublicKey, + errorMessage = 'must match the executing wallet', +): void { + if (!authority.equals(wallet)) { + throw new CCTParamsInvalidError(operation, 'authority', errorMessage) + } +} + +/** + * Asserts `value` is a supported token pool type. + * @throws CCTParamsInvalidError if `value` is not `burn-mint` or `lock-release`. + */ +export function validatePoolType( + operation: string, + param: string, + value: unknown, +): asserts value is TokenPoolType { + if (typeof value !== 'string' || !Object.hasOwn(TOKEN_POOL_PROGRAMS, value)) { + throw new CCTParamsInvalidError(operation, param, 'must be burn-mint or lock-release') + } +} + +/** Resolves a canonical pool type or custom program address. */ +export function resolvePoolProgram(operation: string, params: PoolProgramRef): PublicKey { + // Value semantics: explicit undefined does not count as provided. + const hasPoolType = params.poolType !== undefined + const hasPoolProgramAddress = params.poolProgramAddress !== undefined + if (hasPoolType === hasPoolProgramAddress) { + throw new CCTParamsInvalidError( + operation, + 'poolType', + 'provide exactly one of poolType or poolProgramAddress', + ) + } + + if (hasPoolType) { + validatePoolType(operation, 'poolType', params.poolType) + return resolveTokenPoolProgram(params.poolType) + } + + return parsePublicKey(operation, 'poolProgramAddress', params.poolProgramAddress) +} + +/** + * Asserts `value` is an integer, optionally inside inclusive bounds. + * @throws CCTParamsInvalidError if `value` is not an integer or is outside bounds. + */ +export function validateInteger( + operation: string, + param: string, + value: unknown, + min?: number, + max?: number, +): void { + const validInteger = Number.isInteger(value) + const validMin = min === undefined || (validInteger && Number(value) >= min) + const validMax = max === undefined || (validInteger && Number(value) <= max) + + if (!validInteger || !validMin || !validMax) { + const range = + min !== undefined && max !== undefined + ? ` between ${min} and ${max}` + : min !== undefined + ? ` >= ${min}` + : max !== undefined + ? ` <= ${max}` + : '' + throw new CCTParamsInvalidError(operation, param, `must be an integer${range}`) + } +} + +/** + * Asserts `value` is a bigint, optionally inside inclusive bounds. + * @throws CCTParamsInvalidError if `value` is not a bigint or is outside bounds. + */ +export function validateBigInt( + operation: string, + param: string, + value: unknown, + min?: bigint, + max?: bigint, +): asserts value is bigint { + const validBigInt = typeof value === 'bigint' + const validMin = min === undefined || (validBigInt && value >= min) + const validMax = max === undefined || (validBigInt && value <= max) + + if (!validBigInt || !validMin || !validMax) { + const range = + min !== undefined && max !== undefined + ? ` between ${min} and ${max}` + : min !== undefined + ? ` >= ${min}` + : max !== undefined + ? ` <= ${max}` + : '' + throw new CCTParamsInvalidError(operation, param, `must be a bigint${range}`) + } +} + +/** + * Asserts ALT writable indexes are a non-empty list of byte values when provided. + * @throws CCTParamsInvalidError if indexes are empty or outside byte range. + */ +export function validateWritableIndexes( + operation: string, + param: string, + writableIndexes: unknown, +): void { + if (writableIndexes === undefined) return + if (!Array.isArray(writableIndexes) || writableIndexes.length === 0) { + throw new CCTParamsInvalidError(operation, param, 'must be a non-empty array') + } + + for (const [i, index] of writableIndexes.entries()) { + validateInteger(operation, `${param}[${i}]`, index, 0, 255) + } +} + +/** + * Parses an optionally `0x`-prefixed hex string into bytes, with an optional maximum size. + * @throws CCTParamsInvalidError if `value` is not valid hex or exceeds the requested size. + */ +export function parseHexBytes( + operation: string, + param: string, + value: unknown, + maxBytes?: number, +): Buffer { + const hex = typeof value === 'string' ? value.replace(/^0x/, '') : '' + if ( + typeof value !== 'string' || + !/^(?:[\da-fA-F]{2})*$/.test(hex) || + (maxBytes !== undefined && hex.length / 2 > maxBytes) + ) { + const size = maxBytes === undefined ? '' : ` of at most ${maxBytes} bytes` + throw new CCTParamsInvalidError(operation, param, `must be a hex string${size}`) + } + return Buffer.from(hex, 'hex') +} + +/** + * Parses a non-empty optionally `0x`-prefixed hex string into bytes. + * @throws CCTParamsInvalidError if `value` is not valid non-empty hex or exceeds the requested size. + */ +export function parseNonEmptyHexBytes( + operation: string, + param: string, + value: unknown, + maxBytes?: number, +): Buffer { + const bytes = parseHexBytes(operation, param, value, maxBytes) + if (!bytes.length) throw new CCTParamsInvalidError(operation, param, 'must not be empty') + return bytes +} diff --git a/ccip-sdk/src/cct/token-manager.ts b/ccip-sdk/src/cct/token-manager.ts new file mode 100644 index 00000000..9efe7b42 --- /dev/null +++ b/ccip-sdk/src/cct/token-manager.ts @@ -0,0 +1,18 @@ +/** + * Cross-family CCT manager base, the CCT analogue of core's {@link Chain}. + * Family-specific subclasses hold the chain and expose admin operations. + * + * @packageDocumentation + */ + +import type { Chain } from '../chain.ts' +import type { ChainFamily } from '../networks.ts' + +/** + * Abstract entry point for CCT admin writes on a chain family. Subclasses hold + * the concrete {@link Chain} and delegate to {@link Operation} instances. + */ +export abstract class TokenManager { + /** Chain this manager builds and submits through. */ + abstract readonly chain: Chain +} diff --git a/ccip-sdk/src/errors/codes.ts b/ccip-sdk/src/errors/codes.ts index 48e842f5..7a2c915a 100644 --- a/ccip-sdk/src/errors/codes.ts +++ b/ccip-sdk/src/errors/codes.ts @@ -184,6 +184,14 @@ export const CCIPErrorCode = { // Canton CANTON_API_ERROR: 'CANTON_API_ERROR', + + // CCT (Cross-Chain Token) + CCT_PARAMS_INVALID: 'CCT_PARAMS_INVALID', + CCT_TX_FAILED: 'CCT_TX_FAILED', + CCT_TX_NOT_CONFIRMED: 'CCT_TX_NOT_CONFIRMED', + CCT_CONTRACT_VERSION_UNSUPPORTED: 'CCT_CONTRACT_VERSION_UNSUPPORTED', + CCT_OPERATION_UNSUPPORTED: 'CCT_OPERATION_UNSUPPORTED', + CCT_DATA_DECODE_FAILED: 'CCT_DATA_DECODE_FAILED', } as const /** Union type of all error codes. */ diff --git a/ccip-sdk/src/errors/errors.test.ts b/ccip-sdk/src/errors/errors.test.ts index 98db9aef..927e8996 100644 --- a/ccip-sdk/src/errors/errors.test.ts +++ b/ccip-sdk/src/errors/errors.test.ts @@ -277,6 +277,13 @@ describe('recovery hints', () => { assert.ok(DEFAULT_RECOVERY_HINTS.BLOCK_NOT_FOUND?.includes('Wait')) assert.ok(DEFAULT_RECOVERY_HINTS.HTTP_ERROR?.includes('rate limiting')) }) + + it('should explain how to find a missing token pool state', () => { + assert.equal( + DEFAULT_RECOVERY_HINTS.TOKEN_POOL_STATE_NOT_FOUND, + 'Verify poolType matches the deployed pool, pass poolProgramAddress for a custom pool, and confirm the pool is initialized for this mint on this cluster.', + ) + }) }) describe('getDefaultRecovery', () => { diff --git a/ccip-sdk/src/errors/recovery.ts b/ccip-sdk/src/errors/recovery.ts index 0232813f..ec5ff721 100644 --- a/ccip-sdk/src/errors/recovery.ts +++ b/ccip-sdk/src/errors/recovery.ts @@ -111,7 +111,8 @@ export const DEFAULT_RECOVERY_HINTS: Partial> = { TOKEN_MINT_INVALID: 'The address is not a valid SPL token mint. Ensure the address is owned by TOKEN_PROGRAM_ID or TOKEN_2022_PROGRAM_ID.', TOKEN_AMOUNT_INVALID: 'Token amount must have a valid address and positive amount.', - TOKEN_POOL_STATE_NOT_FOUND: 'TokenPool state PDA not found.', + TOKEN_POOL_STATE_NOT_FOUND: + 'Verify poolType matches the deployed pool, pass poolProgramAddress for a custom pool, and confirm the pool is initialized for this mint on this cluster.', TOKEN_POOL_INFO_NOT_FOUND: 'Check that the token pool is deployed and configured for this lane. Verify supported tokens: https://docs.chain.link/ccip/directory', TOKEN_ACCOUNT_NOT_FOUND: @@ -214,6 +215,20 @@ export const DEFAULT_RECOVERY_HINTS: Partial> = { CANTON_API_ERROR: 'Canton Ledger API returned an error. Verify the party ID is correct, the contract is active, and the Canton node is reachable.', + + // Cross-Chain Token + CCT_PARAMS_INVALID: + 'Verify the operation parameters. See error.context for the field name and reason.', + CCT_TX_FAILED: + 'The CCT transaction failed. Ensure the caller holds the required role for this operation.', + CCT_TX_NOT_CONFIRMED: + 'The transaction was submitted but not confirmed in time. Check the tx hash in error.context before resubmitting; it may still be mined.', + CCT_CONTRACT_VERSION_UNSUPPORTED: + 'This contract version is not supported by the CCT SDK. Check the contract address and its typeAndVersion.', + CCT_OPERATION_UNSUPPORTED: + 'This operation is not available at the contract version in error.context. Verify the contract version supports it.', + CCT_DATA_DECODE_FAILED: + 'Ensure the account belongs to a compatible CCT program and uses the expected data layout.', } /** Returns default recovery hint for error code, or undefined if none. */ diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index 1b4c8e8e..ec7aecaf 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -236,7 +236,7 @@ function encodeAddressToEvm(address: BytesLike): string { } /** typeguard for ethers Signer interface (used for `wallet`s) */ -function isSigner(wallet: unknown): wallet is Signer { +export function isSigner(wallet: unknown): wallet is Signer { return ( typeof wallet === 'object' && wallet !== null && @@ -250,7 +250,7 @@ function isSigner(wallet: unknown): wallet is Signer { * Try sendTransaction() first (works with browser wallets), * fallback to signTransaction() + broadcastTransaction() if unsupported. */ -async function submitTransaction( +export async function submitTransaction( wallet: Signer, tx: TransactionRequest, provider: JsonRpcApiProvider, @@ -478,6 +478,17 @@ export class EVMChain extends Chain { return this.nonces[address]!++ } + /** + * Undo the last {@link nextNonce} increment for a wallet address. + * {@link nextNonce} hands out a nonce optimistically; if the send then fails + * before broadcast, call this so the counter is reused rather than leaving a + * permanent gap that stalls every later transaction. No-op if uncached. + * @param address - Wallet address whose cached nonce to roll back + */ + rollbackNonce(address: string): void { + if (this.nonces[address] != null) this.nonces[address]-- + } + /** * Creates a JSON-RPC provider from a URL. * @param url - WebSocket (wss://) or HTTP (https://) endpoint URL. diff --git a/ccip-sdk/src/solana/__tests__/index.test.ts b/ccip-sdk/src/solana/__tests__/index.test.ts index 3edd3119..a0bf4ace 100644 --- a/ccip-sdk/src/solana/__tests__/index.test.ts +++ b/ccip-sdk/src/solana/__tests__/index.test.ts @@ -1,13 +1,16 @@ import assert from 'node:assert/strict' import { beforeEach, describe, it, mock } from 'node:test' +import { BorshAccountsCoder } from '@coral-xyz/anchor' import { type Connection, PublicKey } from '@solana/web3.js' +import { CCIPDataFormatUnsupportedError } from '../../errors/index.ts' import { type NetworkInfo, ChainFamily, NetworkType } from '../../networks.ts' import { SolanaChain } from '../index.ts' // Create mock functions const mockGetAccountInfo = mock.fn(() => null as any) +const mockGetAddressLookupTable = mock.fn(() => null as any) const mockGetParsedAccountInfo = mock.fn(() => null as any) const mockGetGenesisHash = mock.fn(() => null as any) const mockGetSignaturesForAddress = mock.fn(() => null as any) @@ -17,6 +20,7 @@ const mockConnection = { getGenesisHash: mockGetGenesisHash, getParsedAccountInfo: mockGetParsedAccountInfo, getAccountInfo: mockGetAccountInfo, + getAddressLookupTable: mockGetAddressLookupTable, getSignaturesForAddress: mockGetSignaturesForAddress, } as unknown as Connection @@ -610,6 +614,75 @@ describe('SolanaChain.encodeExtraArgs', () => { }) }) +describe('SolanaChain getRegistryTokenConfig', () => { + const key = (byte: number): PublicKey => { + return new PublicKey(Uint8Array.from({ length: 32 }, () => byte)) + } + + const router = key(1) + const mint = key(2) + const administrator = key(3) + const pendingAdministrator = key(4) + const lookupTable = key(5) + const tokenPool = key(6) + + function tokenAdminRegistryData( + administrator: PublicKey, + pendingAdministrator: PublicKey, + lookupTable: PublicKey, + mint: PublicKey, + ): Buffer { + const data = Buffer.alloc(170) + BorshAccountsCoder.accountDiscriminator('TokenAdminRegistry').copy(data) + data[8] = 2 + administrator.toBuffer().copy(data, 9) + pendingAdministrator.toBuffer().copy(data, 41) + lookupTable.toBuffer().copy(data, 73) + mint.toBuffer().copy(data, 137) + return data + } + + function chainWithLookupTable(lookup: () => Promise): SolanaChain { + return new SolanaChain( + { + getAccountInfo: async () => ({ + data: tokenAdminRegistryData(administrator, pendingAdministrator, lookupTable, mint), + }), + getAddressLookupTable: lookup, + getSignaturesForAddress: async () => [], + } as unknown as Connection, + mockNetworkInfo, + ) + } + + it('returns the configured administrator, pending administrator, and token pool', async () => { + const chain = chainWithLookupTable(async () => ({ + value: { + state: { + addresses: [PublicKey.default, PublicKey.default, PublicKey.default, tokenPool], + }, + }, + })) + + assert.deepEqual(await chain.getRegistryTokenConfig(router.toBase58(), mint.toBase58()), { + administrator: administrator.toBase58(), + pendingAdministrator: pendingAdministrator.toBase58(), + tokenPool: tokenPool.toBase58(), + }) + }) + + it('omits the token pool when lookup-table resolution fails', async () => { + const chain = chainWithLookupTable(async () => { + throw new CCIPDataFormatUnsupportedError('RPC unavailable') + }) + + assert.deepEqual(await chain.getRegistryTokenConfig(router.toBase58(), mint.toBase58()), { + administrator: administrator.toBase58(), + pendingAdministrator: pendingAdministrator.toBase58(), + }) + }) +}) + describe('SolanaChain getExecutionReceipts', () => { let solanaChain: SolanaChain diff --git a/ccip-sdk/src/solana/idl/token-pool-coder.ts b/ccip-sdk/src/solana/idl/token-pool-coder.ts new file mode 100644 index 00000000..088dcf96 --- /dev/null +++ b/ccip-sdk/src/solana/idl/token-pool-coder.ts @@ -0,0 +1,18 @@ +import { type IdlTypes, BorshCoder } from '@coral-xyz/anchor' + +import { IDL as BASE_TOKEN_POOL } from './1.6.0/BASE_TOKEN_POOL.ts' +import { IDL as BURN_MINT_TOKEN_POOL } from './1.6.0/BURN_MINT_TOKEN_POOL.ts' + +// Splice in base IDL types so BaseConfig is defined; required for accounts.decode. +export const TOKEN_POOL_IDL = { + ...BURN_MINT_TOKEN_POOL, + types: BASE_TOKEN_POOL.types, + events: BASE_TOKEN_POOL.events, + errors: [...BASE_TOKEN_POOL.errors, ...BURN_MINT_TOKEN_POOL.errors], +} + +/** Shared state configuration stored by canonical Solana token pools. */ +export type TokenPoolConfig = IdlTypes['BaseConfig'] + +/** Borsh decoder for canonical token pool accounts. */ +export const tokenPoolCoder = new BorshCoder(TOKEN_POOL_IDL) diff --git a/ccip-sdk/src/solana/index.ts b/ccip-sdk/src/solana/index.ts index a493f44d..c26520e7 100644 --- a/ccip-sdk/src/solana/index.ts +++ b/ccip-sdk/src/solana/index.ts @@ -9,7 +9,6 @@ import { Connection, PublicKey, SYSVAR_CLOCK_PUBKEY, - SystemProgram, } from '@solana/web3.js' import BN from 'bn.js' import bs58 from 'bs58' @@ -56,7 +55,6 @@ import { CCIPSplTokenInvalidError, CCIPTokenAccountNotFoundError, CCIPTokenDataParseError, - CCIPTokenNotConfiguredError, CCIPTokenPoolChainConfigNotFoundError, CCIPTokenPoolStateNotFoundError, CCIPTopicsInvalidError, @@ -132,6 +130,10 @@ import { IDL as CCIP_ROUTER_V2_IDL } from './idl/2.0.0/CCIP_ROUTER.ts' import { getTransactionsForAddress } from './logs.ts' import { patchBorsh } from './patchBorsh.ts' import { generateUnsignedCcipSend, getFee } from './send.ts' +import { + decodeTokenAdminRegistryConfig, + getTokenAdminRegistryConfig, +} from './token-admin-registry.ts' import { cacheGetSignaturesForAddress } from './signatures-cache.ts' import { type CCIPMessage_V1_6_Solana, type UnsignedSolanaTx, isWallet } from './types.ts' import { @@ -1829,49 +1831,15 @@ export class SolanaChain extends Chain { pendingAdministrator?: string tokenPool?: string }> { - const registry_ = new PublicKey(registry) - const tokenMint = new PublicKey(token) - - const [tokenAdminRegistryAddr] = PublicKey.findProgramAddressSync( - [Buffer.from('token_admin_registry'), tokenMint.toBuffer()], - registry_, - ) - - const tokenAdminRegistry = await this.connection.getAccountInfo(tokenAdminRegistryAddr) - if (!tokenAdminRegistry) throw new CCIPTokenNotConfiguredError(token, registry) - - const config: { - administrator: string - pendingAdministrator?: string - tokenPool?: string - } = { - administrator: encodeBase58(tokenAdminRegistry.data.subarray(9, 9 + 32)), - } - const pendingAdministrator = new PublicKey(tokenAdminRegistry.data.subarray(41, 41 + 32)) - - // Check if pendingAdministrator is set (not system program address) - if ( - !pendingAdministrator.equals(SystemProgram.programId) && - !pendingAdministrator.equals(PublicKey.default) - ) { - config.pendingAdministrator = pendingAdministrator.toBase58() - } - - // Get token pool from lookup table if available - try { - const lookupTableAddr = new PublicKey(tokenAdminRegistry.data.subarray(73, 73 + 32)) - const lookupTable = await this.connection.getAddressLookupTable(lookupTableAddr) - if (lookupTable.value) { - // tokenPool state PDA is at index [3] - const tokenPoolAddress = lookupTable.value.state.addresses[3] - if (tokenPoolAddress && !tokenPoolAddress.equals(PublicKey.default)) { - config.tokenPool = tokenPoolAddress.toBase58() - } - } - } catch (_err) { - // Token pool may not be configured yet + const router = new PublicKey(registry) + const config = await getTokenAdminRegistryConfig(this.connection, router, new PublicKey(token)) + return { + administrator: config.administrator.toBase58(), + ...(config.pendingAdministrator && { + pendingAdministrator: config.pendingAdministrator.toBase58(), + }), + ...(config.tokenPool && { tokenPool: config.tokenPool.toBase58() }), } - return config } /** @@ -2027,8 +1995,6 @@ export class SolanaChain extends Chain { /** {@inheritDoc Chain.getSupportedTokens} */ async getSupportedTokens(router: string): Promise { - // `mint` offset in TokenAdminRegistry account data; more robust against changes in layout - const mintOffset = 8 + 1 + 32 + 32 + 32 + 16 * 2 // = 137 const router_ = new PublicKey(router) const res = [] for (const acc of await this.connection.getProgramAccounts(router_, { @@ -2041,14 +2007,16 @@ export class SolanaChain extends Chain { }, ], })) { - if (acc.account.data.length < mintOffset + 32) continue - const mint = new PublicKey(acc.account.data.subarray(mintOffset, mintOffset + 32)) - const [derivedPda] = PublicKey.findProgramAddressSync( - [Buffer.from('token_admin_registry'), mint.toBuffer()], - router_, - ) - if (!acc.pubkey.equals(derivedPda)) continue - res.push(mint.toBase58()) + try { + const { mint } = decodeTokenAdminRegistryConfig(acc.account.data) + const [derivedPda] = PublicKey.findProgramAddressSync( + [Buffer.from('token_admin_registry'), mint.toBuffer()], + router_, + ) + if (acc.pubkey.equals(derivedPda)) res.push(mint.toBase58()) + } catch { + // Skip malformed TokenAdminRegistry accounts. + } } return res } diff --git a/ccip-sdk/src/solana/token-admin-registry.ts b/ccip-sdk/src/solana/token-admin-registry.ts new file mode 100644 index 00000000..335ad272 --- /dev/null +++ b/ccip-sdk/src/solana/token-admin-registry.ts @@ -0,0 +1,103 @@ +import { Buffer } from 'buffer' + +import { BorshAccountsCoder } from '@coral-xyz/anchor' +import { type Connection, PublicKey, SystemProgram } from '@solana/web3.js' + +import { CCIPDataFormatUnsupportedError, CCIPTokenNotConfiguredError } from '../errors/index.ts' + +/** Decoded configuration stored in a Solana TokenAdminRegistry account. */ +export type TokenAdminRegistryConfig = { + mint: PublicKey + administrator: PublicKey + pendingAdministrator?: PublicKey + lookupTable?: PublicKey + tokenPool?: PublicKey + writableIndexes: number[] + supportsAutoDerivation: boolean +} + +const TOKEN_ADMIN_REGISTRY_DISCRIMINATOR = + BorshAccountsCoder.accountDiscriminator('TokenAdminRegistry') +const TOKEN_ADMIN_REGISTRY_SIZE = 169 + +/** Decodes the Router's 32-byte MSB-first writable-index bitmap. */ +function decodeWritableIndexes(buf: Buffer): number[] { + const indexes: number[] = [] + for (let byteIndex = 0; byteIndex < 32; byteIndex++) { + const byte = buf[byteIndex] ?? 0 + for (let bit = 0; bit < 8; bit++) { + if (byte & (1 << bit)) { + const bitPosition = (byteIndex % 16) * 8 + bit + indexes.push(byteIndex < 16 ? 127 - bitPosition : 255 - bitPosition) + } + } + } + return indexes.sort((a, b) => a - b) +} + +function isSet(address: PublicKey): boolean { + return !address.equals(PublicKey.default) && !address.equals(SystemProgram.programId) +} + +/** + * Decodes a TokenAdminRegistry account + * + * @param data - Raw TokenAdminRegistry account data. + * @returns Decoded registry configuration, excluding the resolved token pool. + */ +export function decodeTokenAdminRegistryConfig( + data: Buffer, +): Omit { + if ( + data.length < TOKEN_ADMIN_REGISTRY_SIZE || + !data.subarray(0, 8).equals(TOKEN_ADMIN_REGISTRY_DISCRIMINATOR) + ) { + throw new CCIPDataFormatUnsupportedError('invalid TokenAdminRegistry account data') + } + + const pendingAdministrator = new PublicKey(data.subarray(41, 73)) + const lookupTable = new PublicKey(data.subarray(73, 105)) + + return { + mint: new PublicKey(data.subarray(137, 169)), + administrator: new PublicKey(data.subarray(9, 41)), + ...(isSet(pendingAdministrator) && { pendingAdministrator }), + ...(isSet(lookupTable) && { lookupTable }), + writableIndexes: decodeWritableIndexes(data.subarray(105, 137)), + supportsAutoDerivation: data.length > TOKEN_ADMIN_REGISTRY_SIZE && data[169] === 1, + } +} + +/** + * Fetches and decodes a token's TokenAdminRegistry account. + * + * @param connection - Solana RPC connection. + * @param router - Router program that owns the registry account. + * @param mint - Token mint registered with the Router. + * @returns TokenAdminRegistryConfig - The decoded registry configuration. + */ +export async function getTokenAdminRegistryConfig( + connection: Connection, + router: PublicKey, + mint: PublicKey, +): Promise { + const registry = PublicKey.findProgramAddressSync( + [Buffer.from('token_admin_registry'), mint.toBuffer()], + router, + )[0] + + const account = await connection.getAccountInfo(registry) + if (!account) throw new CCIPTokenNotConfiguredError(mint.toBase58(), router.toBase58()) + + const config = decodeTokenAdminRegistryConfig(account.data) + if (!config.lookupTable) return config + + try { + const lookupTable = await connection.getAddressLookupTable(config.lookupTable) + const tokenPool = lookupTable.value?.state.addresses[3] + if (tokenPool && !tokenPool.equals(PublicKey.default)) return { ...config, tokenPool } + } catch { + // Token pool may not be configured yet. + } + return config +} diff --git a/ccip-sdk/src/solana/utils.ts b/ccip-sdk/src/solana/utils.ts index d8a4db1e..268f90da 100644 --- a/ccip-sdk/src/solana/utils.ts +++ b/ccip-sdk/src/solana/utils.ts @@ -46,6 +46,58 @@ export type ResolvedATA = { mintInfo: AccountInfo } +/** + * Fetches and validates a token mint account. + * + * @param connection - Solana connection instance. + * @param mint - Token mint address. + * @returns The validated mint account info. + * @throws CCIPTokenMintNotFoundError If the mint account does not exist. + * @throws CCIPTokenMintInvalidError If the mint is not owned by an SPL Token program. + * + * @example + * ```ts + * const mintInfo = await resolveTokenMint(connection, mint) + * ``` + */ +export async function resolveTokenMint( + connection: Connection, + mint: PublicKey, +): Promise> { + const mintInfo = await connection.getAccountInfo(mint) + if (!mintInfo) throw new CCIPTokenMintNotFoundError(mint.toBase58()) + + if (!mintInfo.owner.equals(TOKEN_PROGRAM_ID) && !mintInfo.owner.equals(TOKEN_2022_PROGRAM_ID)) { + throw new CCIPTokenMintInvalidError(mint.toBase58(), mintInfo.owner.toBase58(), [ + TOKEN_PROGRAM_ID.toBase58(), + TOKEN_2022_PROGRAM_ID.toBase58(), + ]) + } + + return mintInfo +} + +/** + * Resolves and validates the SPL Token program that owns a mint. + * + * @param connection - Solana connection instance. + * @param mint - Token mint address. + * @returns The SPL Token or Token-2022 program address that owns the mint. + * @throws CCIPTokenMintNotFoundError If the mint account does not exist. + * @throws CCIPTokenMintInvalidError If the mint is not owned by an SPL Token program. + * + * @example + * ```ts + * const tokenProgram = await resolveTokenProgram(connection, mint) + * ``` + */ +export async function resolveTokenProgram( + connection: Connection, + mint: PublicKey, +): Promise { + return (await resolveTokenMint(connection, mint)).owner +} + /** * Resolves the Associated Token Account (ATA) for a given mint and owner. * Automatically detects the correct token program (SPL Token vs Token-2022). @@ -67,22 +119,7 @@ export async function resolveATA( mint: PublicKey, owner: PublicKey, ): Promise { - const mintInfo = await connection.getAccountInfo(mint) - if (!mintInfo) { - throw new CCIPTokenMintNotFoundError(mint.toBase58()) - } - - // Validate the mint is owned by a valid token program - const isValidTokenProgram = - mintInfo.owner.equals(TOKEN_PROGRAM_ID) || mintInfo.owner.equals(TOKEN_2022_PROGRAM_ID) - - if (!isValidTokenProgram) { - throw new CCIPTokenMintInvalidError(mint.toBase58(), mintInfo.owner.toBase58(), [ - TOKEN_PROGRAM_ID.toBase58(), - TOKEN_2022_PROGRAM_ID.toBase58(), - ]) - } - + const mintInfo = await resolveTokenMint(connection, mint) // Allow PDAs as owners (for program vaults, etc.) const ata = getAssociatedTokenAddressSync(mint, owner, true, mintInfo.owner) return { diff --git a/ccip-sdk/tsconfig.build.json b/ccip-sdk/tsconfig.build.json index 8a845f9f..78779ee5 100644 --- a/ccip-sdk/tsconfig.build.json +++ b/ccip-sdk/tsconfig.build.json @@ -4,13 +4,6 @@ "outDir": "./dist", "rootDir": "./src" }, - "include": [ - "./src" - ], - "exclude": [ - "node_modules", - "**/*.test.*", - "**/__tests__", - "**/__mocks__" - ] + "include": ["./src"], + "exclude": ["node_modules", "**/*.test.*", "**/__tests__", "**/__mocks__"] } diff --git a/package-lock.json b/package-lock.json index ce6ca693..378c8e85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "ccip-api-ref" ], "devDependencies": { + "@chainlink/contracts-ccip": "2.0.0", "@eslint/js": "^10.0.1", "@types/node": "26.1.2", "brace-expansion": "5.0.9", @@ -106,6 +107,10 @@ "dependencies": { "@aptos-labs/ts-sdk": "^6.3.1", "@coral-xyz/anchor": "^0.29.0", + "@metaplex-foundation/mpl-token-metadata": "3.4.0", + "@metaplex-foundation/umi": "1.5.1", + "@metaplex-foundation/umi-bundle-defaults": "1.5.1", + "@metaplex-foundation/umi-web3js-adapters": "1.5.1", "@mysten/bcs": "^2.1.0", "@mysten/sui": "^2.23.1", "@noble/hashes": "^2.2.0", @@ -543,6 +548,28 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@arbitrum/nitro-contracts": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@arbitrum/nitro-contracts/-/nitro-contracts-3.0.0.tgz", + "integrity": "sha512-7VzNW9TxvrX9iONDDsi7AZlEUPa6z+cjBkB4Mxlnog9VQZAapRC3CdRXyUzHnBYmUhRzyNJdyxkWPw59QGcLmA==", + "dev": true, + "hasInstallScript": true, + "license": "BUSL-1.1", + "dependencies": { + "@offchainlabs/upgrade-executor": "1.1.0-beta.0", + "@openzeppelin/contracts": "4.7.3", + "@openzeppelin/contracts-upgradeable": "4.7.3", + "patch-package": "^6.4.7", + "solady": "0.0.182" + } + }, + "node_modules/@arbitrum/nitro-contracts/node_modules/@openzeppelin/contracts-upgradeable": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.7.3.tgz", + "integrity": "sha512-+wuegAMaLcZnLCJIvrVUDzA9z/Wp93f0Dla/4jJvIhijRrPabjQbZe6fWiECLaJyfn5ci9fqf9vTw3xpQOad2A==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -2292,6 +2319,13 @@ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "license": "MIT" }, + "node_modules/@chainlink/ace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@chainlink/ace/-/ace-1.0.0.tgz", + "integrity": "sha512-lamF+fabw5cyIQ+7PA5QkEl0GyHmmH3lc875jrspo9VxsxiKbMxDcRDGPEuubFQS3Zcx7H/Z80C72+Xm/FgeqA==", + "dev": true, + "license": "BUSL-1.1" + }, "node_modules/@chainlink/ccip-api-ref": { "resolved": "ccip-api-ref", "link": true @@ -2304,6 +2338,115 @@ "resolved": "ccip-sdk", "link": true }, + "node_modules/@chainlink/contracts": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@chainlink/contracts/-/contracts-1.5.0.tgz", + "integrity": "sha512-1fGJwjvivqAxvVOTqZUEXGR54CATtg0vjcXgSIk4Cfoad2nUhSG/qaWHXjLg1CkNTeOoteoxGQcpP/HiA5HsUA==", + "dev": true, + "license": "BUSL-1.1", + "dependencies": { + "@arbitrum/nitro-contracts": "3.0.0", + "@changesets/cli": "^2.29.6", + "@changesets/get-github-info": "^0.6.0", + "@eslint/eslintrc": "^3.3.1", + "@eth-optimism/contracts": "0.6.0", + "@openzeppelin/contracts-4.7.3": "npm:@openzeppelin/contracts@4.7.3", + "@openzeppelin/contracts-4.8.3": "npm:@openzeppelin/contracts@4.8.3", + "@openzeppelin/contracts-4.9.6": "npm:@openzeppelin/contracts@4.9.6", + "@openzeppelin/contracts-5.0.2": "npm:@openzeppelin/contracts@5.0.2", + "@openzeppelin/contracts-5.1.0": "npm:@openzeppelin/contracts@5.1.0", + "@openzeppelin/contracts-upgradeable": "4.9.6", + "@scroll-tech/contracts": "2.0.0", + "@zksync/contracts": "github:matter-labs/era-contracts#446d391d34bdb48255d5f8fef8a8248925fc98b9", + "semver": "^7.7.2" + }, + "engines": { + "node": ">=22", + "pnpm": ">=10" + } + }, + "node_modules/@chainlink/contracts-ccip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@chainlink/contracts-ccip/-/contracts-ccip-2.0.0.tgz", + "integrity": "sha512-P0KvQtZSYC1LevMSS16jOOSsqZG4g0n/MJdcWGmE0Z5U01NVYd1MnTQJOBPsbu1NWR79DBXPXLvyr9tR5y+tiw==", + "dev": true, + "license": "BUSL-1.1", + "dependencies": { + "@chainlink/ace": "1.0.0", + "@chainlink/contracts": "1.5.0", + "@openzeppelin/contracts-4.8.3": "npm:@openzeppelin/contracts@4.8.3", + "@openzeppelin/contracts-5.3.0": "npm:@openzeppelin/contracts@5.3.0" + }, + "engines": { + "node": ">=20", + "pnpm": ">=10" + } + }, + "node_modules/@chainlink/contracts/node_modules/@eth-optimism/contracts": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eth-optimism/contracts/-/contracts-0.6.0.tgz", + "integrity": "sha512-vQ04wfG9kMf1Fwy3FEMqH2QZbgS0gldKhcBeBUPfO8zu68L61VI97UDXmsMQXzTsEAxK8HnokW3/gosl4/NW3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eth-optimism/core-utils": "0.12.0", + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/abstract-signer": "^5.7.0" + }, + "peerDependencies": { + "ethers": "^5" + } + }, + "node_modules/@chainlink/contracts/node_modules/ethers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.8.0.tgz", + "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abi": "5.8.0", + "@ethersproject/abstract-provider": "5.8.0", + "@ethersproject/abstract-signer": "5.8.0", + "@ethersproject/address": "5.8.0", + "@ethersproject/base64": "5.8.0", + "@ethersproject/basex": "5.8.0", + "@ethersproject/bignumber": "5.8.0", + "@ethersproject/bytes": "5.8.0", + "@ethersproject/constants": "5.8.0", + "@ethersproject/contracts": "5.8.0", + "@ethersproject/hash": "5.8.0", + "@ethersproject/hdnode": "5.8.0", + "@ethersproject/json-wallets": "5.8.0", + "@ethersproject/keccak256": "5.8.0", + "@ethersproject/logger": "5.8.0", + "@ethersproject/networks": "5.8.0", + "@ethersproject/pbkdf2": "5.8.0", + "@ethersproject/properties": "5.8.0", + "@ethersproject/providers": "5.8.0", + "@ethersproject/random": "5.8.0", + "@ethersproject/rlp": "5.8.0", + "@ethersproject/sha2": "5.8.0", + "@ethersproject/signing-key": "5.8.0", + "@ethersproject/solidity": "5.8.0", + "@ethersproject/strings": "5.8.0", + "@ethersproject/transactions": "5.8.0", + "@ethersproject/units": "5.8.0", + "@ethersproject/wallet": "5.8.0", + "@ethersproject/web": "5.8.0", + "@ethersproject/wordlists": "5.8.0" + } + }, "node_modules/@chainlink/design-system": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/@chainlink/design-system/-/design-system-0.2.8.tgz", @@ -2315,194 +2458,748 @@ "tailwindcss-animate": "1.0.7" } }, - "node_modules/@chevrotain/types": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", - "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", - "license": "Apache-2.0" - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "node_modules/@changesets/apply-release-plan": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.1.1.tgz", + "integrity": "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==", + "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" + "dependencies": { + "@changesets/config": "^3.1.4", + "@changesets/get-version-range-type": "^0.4.0", + "@changesets/git": "^3.0.4", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "detect-indent": "^6.0.0", + "fs-extra": "^7.0.1", + "lodash.startcase": "^4.4.0", + "outdent": "^0.5.0", + "prettier": "^2.7.1", + "resolve-from": "^5.0.0", + "semver": "^7.5.3" } }, - "node_modules/@coral-xyz/anchor": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.29.0.tgz", - "integrity": "sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==", - "license": "(MIT OR Apache-2.0)", + "node_modules/@changesets/apply-release-plan/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", "dependencies": { - "@coral-xyz/borsh": "^0.29.0", - "@noble/hashes": "^1.3.1", - "@solana/web3.js": "^1.68.0", - "bn.js": "^5.1.2", - "bs58": "^4.0.1", - "buffer-layout": "^1.2.2", - "camelcase": "^6.3.0", - "cross-fetch": "^3.1.5", - "crypto-hash": "^1.3.0", - "eventemitter3": "^4.0.7", - "pako": "^2.0.3", - "snake-case": "^3.0.4", - "superstruct": "^0.15.4", - "toml": "^3.0.0" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, "engines": { - "node": ">=11" + "node": ">=6 <7 || >=8" } }, - "node_modules/@coral-xyz/anchor/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "node_modules/@changesets/apply-release-plan/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@changesets/apply-release-plan/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">=10.13.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/@coral-xyz/anchor/node_modules/base-x": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", - "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "node_modules/@changesets/apply-release-plan/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "^5.0.1" + "engines": { + "node": ">=8" } }, - "node_modules/@coral-xyz/anchor/node_modules/bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "node_modules/@changesets/apply-release-plan/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@changesets/assemble-release-plan": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.10.tgz", + "integrity": "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==", + "dev": true, "license": "MIT", "dependencies": { - "base-x": "^3.0.2" + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "semver": "^7.5.3" } }, - "node_modules/@coral-xyz/anchor/node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" + "node_modules/@changesets/changelog-git": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz", + "integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0" + } }, - "node_modules/@coral-xyz/borsh": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.29.0.tgz", - "integrity": "sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==", - "license": "Apache-2.0", + "node_modules/@changesets/cli": { + "version": "2.31.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.31.1.tgz", + "integrity": "sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==", + "dev": true, + "license": "MIT", "dependencies": { - "bn.js": "^5.1.2", - "buffer-layout": "^1.2.0" - }, - "engines": { - "node": ">=10" + "@changesets/apply-release-plan": "^7.1.1", + "@changesets/assemble-release-plan": "^6.0.10", + "@changesets/changelog-git": "^0.2.1", + "@changesets/config": "^3.1.4", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/get-release-plan": "^4.0.16", + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.7", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@changesets/write": "^0.4.0", + "@inquirer/external-editor": "^1.0.2", + "@manypkg/get-packages": "^1.1.3", + "ansi-colors": "^4.1.3", + "enquirer": "^2.4.1", + "fs-extra": "^7.0.1", + "mri": "^1.2.0", + "package-manager-detector": "^0.2.0", + "picocolors": "^1.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.3", + "spawndamnit": "^3.0.1", + "term-size": "^2.1.0" }, - "peerDependencies": { - "@solana/web3.js": "^1.68.0" + "bin": { + "changeset": "bin.js" } }, - "node_modules/@csstools/cascade-layer-name-parser": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", - "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], + "node_modules/@changesets/cli/node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, "engines": { "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", + "node_modules/@changesets/cli/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, "engines": { - "node": ">=18" + "node": ">=6 <7 || >=8" } }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], + "node_modules/@changesets/cli/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], + "node_modules/@changesets/cli/node_modules/package-manager-detector": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", + "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", + "dev": true, "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "quansync": "^0.2.7" } }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "node_modules/@changesets/cli/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@changesets/cli/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@changesets/config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.4.tgz", + "integrity": "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/logger": "^0.1.1", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1", + "micromatch": "^4.0.8" + } + }, + "node_modules/@changesets/config/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/config/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@changesets/config/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@changesets/errors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", + "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", + "dev": true, + "license": "MIT", + "dependencies": { + "extendable-error": "^0.1.5" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.4.tgz", + "integrity": "sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "picocolors": "^1.1.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/get-github-info": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.6.0.tgz", + "integrity": "sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dataloader": "^1.4.0", + "node-fetch": "^2.5.0" + } + }, + "node_modules/@changesets/get-github-info/node_modules/dataloader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz", + "integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@changesets/get-release-plan": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.16.tgz", + "integrity": "sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/assemble-release-plan": "^6.0.10", + "@changesets/config": "^3.1.4", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.7", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/get-version-range-type": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz", + "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@changesets/git": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.4.tgz", + "integrity": "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@manypkg/get-packages": "^1.1.3", + "is-subdir": "^1.1.1", + "micromatch": "^4.0.8", + "spawndamnit": "^3.0.1" + } + }, + "node_modules/@changesets/logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz", + "integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/parse": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.3.tgz", + "integrity": "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "js-yaml": "^4.1.1" + } + }, + "node_modules/@changesets/pre": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz", + "integrity": "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1" + } + }, + "node_modules/@changesets/pre/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/pre/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@changesets/pre/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@changesets/read": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.7.tgz", + "integrity": "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/parse": "^0.4.3", + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "p-filter": "^2.1.0", + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/read/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/read/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@changesets/read/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz", + "integrity": "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/types": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz", + "integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@changesets/write": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz", + "integrity": "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "human-id": "^4.1.1", + "prettier": "^2.7.1" + } + }, + "node_modules/@changesets/write/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@changesets/write/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@changesets/write/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/@changesets/write/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@coral-xyz/anchor": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@coral-xyz/anchor/-/anchor-0.29.0.tgz", + "integrity": "sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@coral-xyz/borsh": "^0.29.0", + "@noble/hashes": "^1.3.1", + "@solana/web3.js": "^1.68.0", + "bn.js": "^5.1.2", + "bs58": "^4.0.1", + "buffer-layout": "^1.2.2", + "camelcase": "^6.3.0", + "cross-fetch": "^3.1.5", + "crypto-hash": "^1.3.0", + "eventemitter3": "^4.0.7", + "pako": "^2.0.3", + "snake-case": "^3.0.4", + "superstruct": "^0.15.4", + "toml": "^3.0.0" + }, + "engines": { + "node": ">=11" + } + }, + "node_modules/@coral-xyz/anchor/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@coral-xyz/anchor/node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@coral-xyz/anchor/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/@coral-xyz/anchor/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/@coral-xyz/borsh": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@coral-xyz/borsh/-/borsh-0.29.0.tgz", + "integrity": "sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.1.2", + "buffer-layout": "^1.2.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@solana/web3.js": "^1.68.0" + } + }, + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", + "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "funding": [ { @@ -5655,6 +6352,87 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", @@ -5700,6 +6478,31 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eth-optimism/core-utils": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eth-optimism/core-utils/-/core-utils-0.12.0.tgz", + "integrity": "sha512-qW+7LZYCz7i8dRa7SRlUKIo1VBU8lvN0HeXCxJR+z+xtMzMQpPds20XJNCMclszxYQHkXY00fOT6GvFw9ZL6nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "^5.7.0", + "@ethersproject/abstract-provider": "^5.7.0", + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/constants": "^5.7.0", + "@ethersproject/contracts": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/keccak256": "^5.7.0", + "@ethersproject/properties": "^5.7.0", + "@ethersproject/providers": "^5.7.0", + "@ethersproject/rlp": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@ethersproject/web": "^5.7.0", + "bufio": "^1.0.7", + "chai": "^4.3.4" + } + }, "node_modules/@ethers-ext/signer-ledger": { "version": "6.0.0-beta.1", "resolved": "https://registry.npmjs.org/@ethers-ext/signer-ledger/-/signer-ledger-6.0.0-beta.1.tgz", @@ -5836,6 +6639,27 @@ "@ethersproject/bytes": "^5.8.0" } }, + "node_modules/@ethersproject/basex": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.8.0.tgz", + "integrity": "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, "node_modules/@ethersproject/bignumber": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", @@ -5895,6 +6719,35 @@ "@ethersproject/bignumber": "^5.8.0" } }, + "node_modules/@ethersproject/contracts": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.8.0.tgz", + "integrity": "sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abi": "^5.8.0", + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0" + } + }, "node_modules/@ethersproject/hash": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", @@ -5922,6 +6775,79 @@ "@ethersproject/strings": "^5.8.0" } }, + "node_modules/@ethersproject/hdnode": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.8.0.tgz", + "integrity": "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz", + "integrity": "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/json-wallets/node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@ethersproject/keccak256": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", @@ -5977,6 +6903,28 @@ "@ethersproject/logger": "^5.8.0" } }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz", + "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/sha2": "^5.8.0" + } + }, "node_modules/@ethersproject/properties": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", @@ -5996,6 +6944,66 @@ "@ethersproject/logger": "^5.8.0" } }, + "node_modules/@ethersproject/providers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.8.0.tgz", + "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0", + "bech32": "1.1.4", + "ws": "8.18.0" + } + }, + "node_modules/@ethersproject/random": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.8.0.tgz", + "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, "node_modules/@ethersproject/rlp": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", @@ -6016,6 +7024,28 @@ "@ethersproject/logger": "^5.8.0" } }, + "node_modules/@ethersproject/sha2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz", + "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "hash.js": "1.1.7" + } + }, "node_modules/@ethersproject/signing-key": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", @@ -6035,15 +7065,90 @@ "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", - "bn.js": "^5.2.1", - "elliptic": "6.6.1", - "hash.js": "1.1.7" + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/solidity": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.8.0.tgz", + "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" } }, - "node_modules/@ethersproject/strings": { + "node_modules/@ethersproject/units": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", - "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.8.0.tgz", + "integrity": "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==", + "dev": true, "funding": [ { "type": "individual", @@ -6055,16 +7160,18 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "@ethersproject/bytes": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethersproject/transactions": { + "node_modules/@ethersproject/wallet": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", - "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.8.0.tgz", + "integrity": "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA==", + "dev": true, "funding": [ { "type": "individual", @@ -6076,16 +7183,23 @@ } ], "license": "MIT", + "peer": true, "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", "@ethersproject/address": "^5.8.0", "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", - "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/json-wallets": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", - "@ethersproject/rlp": "^5.8.0", - "@ethersproject/signing-key": "^5.8.0" + "@ethersproject/random": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" } }, "node_modules/@ethersproject/web": { @@ -6111,6 +7225,31 @@ "@ethersproject/strings": "^5.8.0" } }, + "node_modules/@ethersproject/wordlists": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.8.0.tgz", + "integrity": "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, "node_modules/@exodus/schemasafe": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", @@ -7453,6 +8592,174 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "license": "MIT" }, + "node_modules/@manypkg/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@types/node": "^12.7.1", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0" + } + }, + "node_modules/@manypkg/find-root/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/find-root/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@manypkg/find-root/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@manypkg/find-root/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@manypkg/find-root/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", + "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@changesets/types": "^4.0.1", + "@manypkg/find-root": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "read-yaml-file": "^1.1.0" + } + }, + "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", + "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/get-packages/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/get-packages/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@manypkg/get-packages/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/@mdx-js/mdx": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", @@ -7490,30 +8797,236 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/@mdx-js/react": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", - "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.1.tgz", + "integrity": "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, + "node_modules/@metaplex-foundation/mpl-token-metadata": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-3.4.0.tgz", + "integrity": "sha512-AxBAYCK73JWxY3g9//z/C9krkR0t1orXZDknUPS4+GjwGH2vgPfsk04yfZ31Htka2AdS9YE/3wH7sMUBHKn9Rg==", + "license": "Apache-2.0", + "dependencies": { + "@metaplex-foundation/mpl-toolbox": "^0.10.0" + }, + "peerDependencies": { + "@metaplex-foundation/umi": ">= 0.8.2 <= 1" + } + }, + "node_modules/@metaplex-foundation/mpl-toolbox": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-toolbox/-/mpl-toolbox-0.10.0.tgz", + "integrity": "sha512-84KD1L5cFyw5xnntHwL4uPwfcrkKSiwuDeypiVr92qCUFuF3ZENa2zlFVPu+pQcjTlod2LmEX3MhBmNjRMpdKg==", + "license": "Apache-2.0", + "peerDependencies": { + "@metaplex-foundation/umi": ">= 0.8.2 <= 1" + } + }, + "node_modules/@metaplex-foundation/umi": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi/-/umi-1.5.1.tgz", + "integrity": "sha512-ONRv5a0kv+23AMlR8oyFBHnjVg3o3N8pUfFcV4gzbg6OgZf87zHsPWBfED3OTJqx267v1bEn6d6DABXNFq9Z3A==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-options": "^1.5.1", + "@metaplex-foundation/umi-public-keys": "^1.5.1", + "@metaplex-foundation/umi-serializers": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-bundle-defaults": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-bundle-defaults/-/umi-bundle-defaults-1.5.1.tgz", + "integrity": "sha512-7qoXenAkQbcj468HGAeLZDyg3eEhcS9rWAnGqjnKgWOlL1czL2Qwho0FEtqOv57IHwAJSTpbHbcvABmdpTjjdw==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-downloader-http": "^1.5.1", + "@metaplex-foundation/umi-eddsa-web3js": "^1.5.1", + "@metaplex-foundation/umi-http-fetch": "^1.5.1", + "@metaplex-foundation/umi-program-repository": "^1.5.1", + "@metaplex-foundation/umi-rpc-chunk-get-accounts": "^1.5.1", + "@metaplex-foundation/umi-rpc-web3js": "^1.5.1", + "@metaplex-foundation/umi-serializer-data-view": "^1.5.1", + "@metaplex-foundation/umi-transaction-factory-web3js": "^1.5.1" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@metaplex-foundation/umi-downloader-http": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-downloader-http/-/umi-downloader-http-1.5.1.tgz", + "integrity": "sha512-1s9gSTaDtwELyxBRE6Wmdr3xWeb4Z1uU04dj3Hg8VU+TN6/3wchh93+rIGZT5D3zzdh4+yPxdYV+4ZEr3T5glQ==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-eddsa-web3js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-eddsa-web3js/-/umi-eddsa-web3js-1.5.1.tgz", + "integrity": "sha512-ZlzmXXAa1Ujk00G5TmqXM81J25+k/8sqt0zxBUlLTUSOxzlhxhlUKdErIhpHazbKq+eGck+Onm17oAwVKdKAcw==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-web3js-adapters": "^1.5.1", + "@noble/curves": "^1.0.0", + "yaml": "^2.7.0" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@metaplex-foundation/umi-http-fetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-http-fetch/-/umi-http-fetch-1.5.1.tgz", + "integrity": "sha512-AOjZJo3Ua4a2FvgA85x5f0TkMSb+13Ao3uLIQ9FbScV42kqZnDox8KjJ7tKm1ZtYDlCYD0pSFMKPOC9NPDnHDg==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.7" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-options": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-options/-/umi-options-1.5.1.tgz", + "integrity": "sha512-ZE6uXgFA3rElFq4gJxZM2diAqZdFqL65bOnAggwdnnei5XXRzFyNF16wYSqlHnPLvG6ohRHWiXww8d2Mb83xFg==", + "license": "MIT" + }, + "node_modules/@metaplex-foundation/umi-program-repository": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-program-repository/-/umi-program-repository-1.5.1.tgz", + "integrity": "sha512-E5W0IjwFgDGuBTshISbbEh/s8deqxcOzzEjOOlYdMXnevVsfNLwBBIAY4NPJg3v5vpFlKODwUGB5BxCUVthzJg==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-public-keys": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-public-keys/-/umi-public-keys-1.5.1.tgz", + "integrity": "sha512-joTnI1mRtYRfIaTo98uaYRjBPszsdyHuq0vvd6QbSX+MPvu3enkWi+UicuykEc3VXd5tcGdNMiGSx4jgXG6pkw==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-serializers-encodings": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-rpc-chunk-get-accounts": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-rpc-chunk-get-accounts/-/umi-rpc-chunk-get-accounts-1.5.1.tgz", + "integrity": "sha512-3dnGobT1Xwul7fXzQr8660UHSnFOCWEed4T449oNekrVsHp2o00fdOqjXwo11DYhS1rjm+gbzRSazRKb62uF2Q==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-rpc-web3js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-rpc-web3js/-/umi-rpc-web3js-1.5.1.tgz", + "integrity": "sha512-CxHyruh2gW2b/ZOwHFFtooOgtu9hBrOJTd3HUMtD/jpaturApa3itsL/zNt4K34tELzVIUL7N78LDjNpzbu9Kw==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-web3js-adapters": "^1.5.1" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@metaplex-foundation/umi-serializer-data-view": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializer-data-view/-/umi-serializer-data-view-1.5.1.tgz", + "integrity": "sha512-9Wxqk3bGVJ0xNmHhHrOUhdu/90Q1IT3FZRZN4eGckb0sf7Bgls7kBTkFfgXFmUh2VBnE0GnnncXeHKtop5RSFA==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-serializers": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers/-/umi-serializers-1.5.1.tgz", + "integrity": "sha512-scXciBylbJ4iwfxOF1Xx2XiBzoYUD8fSKWTsMal5Rj1hMRDe6b2XZcsBOjio61iAr8aTtFPmKpqxeBdLwmQ0ZQ==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-options": "^1.5.1", + "@metaplex-foundation/umi-public-keys": "^1.5.1", + "@metaplex-foundation/umi-serializers-core": "^1.5.1", + "@metaplex-foundation/umi-serializers-encodings": "^1.5.1", + "@metaplex-foundation/umi-serializers-numbers": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-serializers-core": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-core/-/umi-serializers-core-1.5.1.tgz", + "integrity": "sha512-6nYsbTCLq421x7JT1B3/iNgPpSARj/wL9naoKbOreHrk2ip/4R7vQstVRMl0Gx+Hv2tHnEIbFo3JBtWyC377Qw==", + "license": "MIT" + }, + "node_modules/@metaplex-foundation/umi-serializers-encodings": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-encodings/-/umi-serializers-encodings-1.5.1.tgz", + "integrity": "sha512-cVvwWmREE/Pmvjvsd50F18P53HDT0vzZECD6uYWIVzxgwpOiRDFu6r/vGbweomHoWzfTvuU6hiKuKv2KsOoXQA==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-serializers-core": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-serializers-numbers": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-numbers/-/umi-serializers-numbers-1.5.1.tgz", + "integrity": "sha512-7DVF1VJIdT44Pe6qWKaqGu4YVgE10OeLMYpm7C16SujSBgQGB/I2bh8NBifyH2R3oHhoyfE9qgIKB3dgRazN6A==", "license": "MIT", "dependencies": { - "@types/mdx": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "@metaplex-foundation/umi-serializers-core": "^1.5.1" + } + }, + "node_modules/@metaplex-foundation/umi-transaction-factory-web3js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-transaction-factory-web3js/-/umi-transaction-factory-web3js-1.5.1.tgz", + "integrity": "sha512-g4NfvtnmXtH1Q/Y9LdCsFtDRHQZmZWW7uKz+N9a+IVsJTTvpWFALMHm66dFDQGa0ExAYxAj7j6uZH2qDn0zarA==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-web3js-adapters": "^1.5.1" }, "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" } }, - "node_modules/@mermaid-js/parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.1.tgz", - "integrity": "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==", + "node_modules/@metaplex-foundation/umi-web3js-adapters": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-web3js-adapters/-/umi-web3js-adapters-1.5.1.tgz", + "integrity": "sha512-6W3JElD0B0EbgHofVKqk4PbP/JDrUHIKWciM7tEuXTDXbuXbSECDe7qlTU0JZXmVZNfYufI6FHnkCfPys2ZnIQ==", "license": "MIT", "dependencies": { - "@chevrotain/types": "~11.1.2" + "buffer": "^6.0.3" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" } }, "node_modules/@microsoft/tsdoc": { @@ -8047,6 +9560,86 @@ "node": ">= 8" } }, + "node_modules/@offchainlabs/upgrade-executor": { + "version": "1.1.0-beta.0", + "resolved": "https://registry.npmjs.org/@offchainlabs/upgrade-executor/-/upgrade-executor-1.1.0-beta.0.tgz", + "integrity": "sha512-mpn6PHjH/KDDjNX0pXHEKdyv8m6DVGQiI2nGzQn0JbM1nOSHJpWx6fvfjtH7YxHJ6zBZTcsKkqGkFKDtCfoSLw==", + "dev": true, + "license": "Apache 2.0", + "dependencies": { + "@openzeppelin/contracts": "4.7.3", + "@openzeppelin/contracts-upgradeable": "4.7.3" + } + }, + "node_modules/@offchainlabs/upgrade-executor/node_modules/@openzeppelin/contracts-upgradeable": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.7.3.tgz", + "integrity": "sha512-+wuegAMaLcZnLCJIvrVUDzA9z/Wp93f0Dla/4jJvIhijRrPabjQbZe6fWiECLaJyfn5ci9fqf9vTw3xpQOad2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@openzeppelin/contracts": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.7.3.tgz", + "integrity": "sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@openzeppelin/contracts-4.7.3": { + "name": "@openzeppelin/contracts", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.7.3.tgz", + "integrity": "sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@openzeppelin/contracts-4.8.3": { + "name": "@openzeppelin/contracts", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.8.3.tgz", + "integrity": "sha512-bQHV8R9Me8IaJoJ2vPG4rXcL7seB7YVuskr4f+f5RyOStSZetwzkWtoqDMl5erkBJy0lDRUnIR2WIkPiC0GJlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@openzeppelin/contracts-4.9.6": { + "name": "@openzeppelin/contracts", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.9.6.tgz", + "integrity": "sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@openzeppelin/contracts-5.0.2": { + "name": "@openzeppelin/contracts", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.2.tgz", + "integrity": "sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@openzeppelin/contracts-5.1.0": { + "name": "@openzeppelin/contracts", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.1.0.tgz", + "integrity": "sha512-p1ULhl7BXzjjbha5aqst+QMLY+4/LCWADXOCsmLHRM77AqiPjnd9vvUN9sosUfhL9JGKpZ0TjEGxgvnizmWGSA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@openzeppelin/contracts-5.3.0": { + "name": "@openzeppelin/contracts", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.3.0.tgz", + "integrity": "sha512-zj/KGoW7zxWUE8qOI++rUM18v+VeLTTzKs/DJFkSzHpQFPD/jKKF0TrMxBfGLl3kpdELCNccvB3zmofSzm4nlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@openzeppelin/contracts-upgradeable": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.6.tgz", + "integrity": "sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@parcel/watcher": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", @@ -8714,6 +10307,13 @@ } } }, + "node_modules/@scroll-tech/contracts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scroll-tech/contracts/-/contracts-2.0.0.tgz", + "integrity": "sha512-O8sVaA/bVKH/mp+bBfUjZ/vYr5mdBExCpKRLre4r9TbXTtiaY9Uo5xU8dcG3weLxyK0BZqDTP2aCNp4Q0f7SeA==", + "dev": true, + "license": "MIT" + }, "node_modules/@scure/base": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.3.0.tgz", @@ -11248,6 +12848,31 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "license": "Apache-2.0" }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/@zksync/contracts": { + "name": "era-contracts", + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/matter-labs/era-contracts.git#446d391d34bdb48255d5f8fef8a8248925fc98b9", + "integrity": "sha512-KhgPVqd/MgV/ICUEsQf1uyL321GNPqsyHSAPMCaa9vW94fbuQK6RwMWoyQOPlZP17cQD8tzLNCSXqz73652kow==", + "dev": true, + "workspaces": { + "packages": [ + "l1-contracts", + "l2-contracts", + "system-contracts", + "gas-bound-caller" + ], + "nohoist": [ + "**/@openzeppelin/**" + ] + } + }, "node_modules/abitype": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.3.0.tgz", @@ -11536,6 +13161,16 @@ "node": ">=8" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", @@ -11663,6 +13298,16 @@ "node": ">=12.0.0" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", @@ -11684,6 +13329,16 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/atomically": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", @@ -11880,6 +13535,26 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "license": "MIT" }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/better-path-resolve": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", + "integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-windows": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -12262,6 +13937,16 @@ "node": ">=6.14.2" } }, + "node_modules/bufio": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bufio/-/bufio-1.2.3.tgz", + "integrity": "sha512-5Tt66bRzYUSlVZatc0E92uDenreJ+DpTBmSAUwL4VSxJn3e6cUyYwx+PoqML0GRZatgA/VX8ybhxItF8InZgqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -12509,6 +14194,25 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -12611,6 +14315,19 @@ "node": ">=4.0.0" } }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -14381,6 +16098,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -14544,6 +16274,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -15067,6 +16807,43 @@ "node": ">=10.13.0" } }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/enquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -16138,6 +17915,13 @@ "node": ">=0.10.0" } }, + "node_modules/extendable-error": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", + "integrity": "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==", + "dev": true, + "license": "MIT" + }, "node_modules/eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", @@ -16503,6 +18287,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -16744,6 +18538,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -16931,6 +18735,19 @@ "node": ">=10" } }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -17693,6 +19510,16 @@ "node": ">= 6" } }, + "node_modules/human-id": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.2.0.tgz", + "integrity": "sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -18244,6 +20071,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-subdir": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz", + "integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "better-path-resolve": "1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -18263,6 +20103,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -18947,6 +20797,13 @@ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "license": "MIT" }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -18975,6 +20832,16 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -21676,6 +23543,16 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -21804,6 +23681,13 @@ "node": ">= 10" } }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -22628,9 +24512,26 @@ "word-wrap": "^1.2.5" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.8.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, + "node_modules/outdent": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", + "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==", + "dev": true, + "license": "MIT" + }, "node_modules/ox": { "version": "0.14.34", "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.34.tgz", @@ -22708,6 +24609,29 @@ "node": ">=8" } }, + "node_modules/p-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", + "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-map": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-filter/node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -22815,6 +24739,16 @@ "node": ">=8" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", @@ -23024,6 +24958,182 @@ "tslib": "^2.0.3" } }, + "node_modules/patch-package": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.5.1.tgz", + "integrity": "sha512-I/4Zsalfhc6bphmJTlrLoOcAF87jcxko4q0qsv4bGcurbr8IskEOtdnt9iCmsQVGL1B+iUhSQqweyTLJfCF9rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "cross-spawn": "^6.0.5", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "is-ci": "^2.0.0", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^5.6.0", + "slash": "^2.0.0", + "tmp": "^0.0.33", + "yaml": "^1.10.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=10", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/patch-package/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/patch-package/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/patch-package/node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/patch-package/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/patch-package/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/patch-package/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/patch-package/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/patch-package/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/patch-package/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -23111,6 +25221,16 @@ "node": ">=8" } }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -25368,6 +27488,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -25722,6 +27859,32 @@ "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", "license": "MIT" }, + "node_modules/read-yaml-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", + "integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.6.1", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/read-yaml-file/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -26371,6 +28534,55 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -26707,6 +28919,14 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/search-insights": { "version": "2.17.3", "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", @@ -27417,6 +29637,13 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/solady": { + "version": "0.0.182", + "resolved": "https://registry.npmjs.org/solady/-/solady-0.0.182.tgz", + "integrity": "sha512-FW6xo1akJoYpkXMzu58/56FcNU3HYYNamEbnFO3iSibXk0nSHo0DV2Gu/zI3FPg3So5CCX6IYli1TT1IWATnvg==", + "dev": true, + "license": "MIT" + }, "node_modules/sort-css-media-queries": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz", @@ -27473,6 +29700,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/spawndamnit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz", + "integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "cross-spawn": "^7.0.5", + "signal-exit": "^4.0.1" + } + }, "node_modules/spdx-exceptions": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", @@ -27648,6 +29886,16 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-bom-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", @@ -28128,6 +30376,19 @@ "node": ">=6" } }, + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/terser": { "version": "5.50.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", @@ -28356,6 +30617,19 @@ "node": "^18.0.0 || >=20.0.0" } }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -28559,6 +30833,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", diff --git a/package.json b/package.json index 3e3c9e40..2493e555 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "prepare": "npm run build" }, "devDependencies": { + "@chainlink/contracts-ccip": "2.0.0", "@eslint/js": "^10.0.1", "@types/node": "26.1.2", "brace-expansion": "5.0.9",