From 1c365782bc204406c4106b8bf1ad3d7091d73d2f Mon Sep 17 00:00:00 2001 From: Pedro Barbosa Date: Fri, 12 Jun 2026 17:25:10 +0100 Subject: [PATCH 01/87] Initialize CCT SDK with setPool --- ccip-cli/src/index.ts | 2 +- ccip-sdk/package.json | 4 + ccip-sdk/src/api/index.ts | 2 +- ccip-sdk/src/cct/evm/index.test.ts | 131 ++ ccip-sdk/src/cct/evm/index.ts | 70 + ccip-sdk/src/cct/evm/operations/set-pool.ts | 84 + ccip-sdk/src/cct/evm/submit.test.ts | 122 ++ ccip-sdk/src/cct/evm/submit.ts | 87 + ccip-sdk/src/cct/evm/validate.test.ts | 30 + ccip-sdk/src/cct/evm/validate.ts | 23 + ccip-sdk/src/cct/token-manager.ts | 18 + ccip-sdk/src/errors/codes.ts | 5 + ccip-sdk/src/errors/index.ts | 7 + ccip-sdk/src/errors/recovery.ts | 7 + ccip-sdk/src/errors/specialized.ts | 60 + ccip-sdk/src/evm/index.ts | 4 +- pnpm-lock.yaml | 1891 +++++++++++++++++++ 17 files changed, 2543 insertions(+), 4 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/index.test.ts create mode 100644 ccip-sdk/src/cct/evm/index.ts create mode 100644 ccip-sdk/src/cct/evm/operations/set-pool.ts create mode 100644 ccip-sdk/src/cct/evm/submit.test.ts create mode 100644 ccip-sdk/src/cct/evm/submit.ts create mode 100644 ccip-sdk/src/cct/evm/validate.test.ts create mode 100644 ccip-sdk/src/cct/evm/validate.ts create mode 100644 ccip-sdk/src/cct/token-manager.ts create mode 100644 pnpm-lock.yaml diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 97ba39eb8..03bb19de1 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -28,7 +28,7 @@ Error.stackTraceLimit = 50 // show more stack frames for better debugging // generate:nofail // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -const VERSION = '1.7.1-90c0438' +const VERSION = '1.7.1-2807dab' // generate:end const require = createRequire(import.meta.url) diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index 21021ab85..29ae101fa 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -27,6 +27,10 @@ "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" + }, "./src/*": "./src/*" }, "scripts": { diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index a8186e284..36ffd118f 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -58,7 +58,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.7.1-90c0438' +export const SDK_VERSION = '1.7.1-2807dab' // generate:end /** SDK telemetry header name */ 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 000000000..1375ae9ac --- /dev/null +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -0,0 +1,131 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, id } from 'ethers' + +import { EVMTokenManager } from './index.ts' +import { CCIPCctParamsInvalidError, CCIPWalletInvalidError } from '../../errors/index.ts' +import type { EVMChain } from '../../evm/index.ts' +import { ChainFamily } from '../../networks.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) + +/** Minimal EVMChain stub — only the members EVMTokenManager touches. */ +function stubChain(overrides: Partial = {}): EVMChain { + return { + provider: {} as never, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + getTokenAdminRegistryFor: (_address: string) => Promise.resolve(TAR), + ...overrides, + } as unknown as EVMChain +} + +const SET_POOL_SELECTOR = id('setPool(address,address)').slice(0, 10) +const EXPECTED_DATA = new Interface([ + 'function setPool(address localToken, address pool)', +]).encodeFunctionData('setPool', [TOKEN, POOL]) + +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('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, + routerAddress: 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, + routerAddress: 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, + routerAddress: 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, + routerAddress: ROUTER, + }), + (err: unknown) => + err instanceof CCIPCctParamsInvalidError && + err.context.operation === 'setPool' && + err.context.param === 'tokenAddress', + ) + assert.equal(called, false, 'validation fails before TAR discovery') + }) + }) + + describe('setPool', () => { + it('rejects a non-signer wallet', async () => { + const cct = EVMTokenManager.fromChain(stubChain()) + await assert.rejects( + () => + cct.setPool({ + tokenAddress: TOKEN, + poolAddress: POOL, + routerAddress: ROUTER, + wallet: {}, + }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts new file mode 100644 index 000000000..dcd1af4c4 --- /dev/null +++ b/ccip-sdk/src/cct/evm/index.ts @@ -0,0 +1,70 @@ +/** + * EVM Cross-Chain Token (CCT) admin operations on the TokenAdminRegistry. + * {@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 { TokenManager } from '../token-manager.ts' +import * as SetPool from './operations/set-pool.ts' + +/** CCT admin operations for EVM chains, delegating each op to `./operations`. */ +export class EVMTokenManager extends TokenManager { + readonly chain: EVMChain + + /** Wraps the chain this manager builds and submits through. */ + 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 `setPool` tx (for multisig / offline signing). + * @throws {@link CCIPCctParamsInvalidError} if any param is invalid + */ + generateUnsignedSetPool( + opts: SetPool.SetPoolParams & { sender?: string }, + ): Promise { + return SetPool.generate(this.chain, opts) + } + + /** + * Registers a pool, signing + submitting with `opts.wallet` (the token admin). + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCIPCctParamsInvalidError} if any param is invalid + * @throws {@link CCIPCctTxFailedError} if the tx reverts or fails + */ + setPool(opts: SetPool.SetPoolParams & { wallet: unknown }): Promise { + return SetPool.execute(this.chain, opts) + } +} diff --git a/ccip-sdk/src/cct/evm/operations/set-pool.ts b/ccip-sdk/src/cct/evm/operations/set-pool.ts new file mode 100644 index 000000000..22a9af8c2 --- /dev/null +++ b/ccip-sdk/src/cct/evm/operations/set-pool.ts @@ -0,0 +1,84 @@ +/** + * `setPool` — registers a pool for a token in the TokenAdminRegistry. + * Version-independent (v1.5/v1.6/v2.0 share one encoding). + * + * @packageDocumentation + */ + +import { type TransactionRequest, Interface } from 'ethers' + +import TokenAdminRegistryABI from '../../../evm/abi/TokenAdminRegistry_1_5.ts' +import type { EVMChain } from '../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../evm/types.ts' +import { ChainFamily } from '../../../networks.ts' +import type { CctTxResult } from '../../token-manager.ts' +import { submit } from '../submit.ts' +import { validateAddress } from '../validate.ts' + +export const OPERATION = 'setPool' + +/** Parameters for `setPool`. */ +export type SetPoolParams = { + tokenAddress: string + /** Pool to register; zero address delists the token. */ + poolAddress: string + /** Router — used to discover the TokenAdminRegistry. */ + routerAddress: string +} + +/** Result of `setPool`. */ +export type SetPoolResult = CctTxResult + +/** + * Validates {@link SetPoolParams} before any RPC. + * @throws {@link CCIPCctParamsInvalidError} if any address is invalid + */ +function validate(params: SetPoolParams): void { + validateAddress(OPERATION, 'tokenAddress', params.tokenAddress) + validateAddress(OPERATION, 'poolAddress', params.poolAddress) + validateAddress(OPERATION, 'routerAddress', params.routerAddress) +} + +/** Encodes the `setPool(localToken, pool)` calldata. */ +export function encode(params: SetPoolParams): string { + return new Interface(TokenAdminRegistryABI).encodeFunctionData('setPool', [ + params.tokenAddress, + params.poolAddress, + ]) +} + +/** + * Builds an unsigned `setPool` tx on the discovered TokenAdminRegistry; set + * `sender` to populate `from`. + * @throws {@link CCIPCctParamsInvalidError} if any param is invalid + */ +export async function generate( + chain: EVMChain, + opts: SetPoolParams & { sender?: string }, +): Promise { + validate(opts) + + const to = await chain.getTokenAdminRegistryFor(opts.routerAddress) + const tx: TransactionRequest = { to, data: encode(opts) } + if (opts.sender) tx.from = opts.sender + + chain.logger.debug( + `${OPERATION}: TAR = ${to}, token = ${opts.tokenAddress}, pool = ${opts.poolAddress}`, + ) + return { family: ChainFamily.EVM, transactions: [tx] } +} + +/** + * Builds and submits `setPool` with `opts.wallet` (the token admin). + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCIPCctParamsInvalidError} if any param is invalid + * @throws {@link CCIPCctTxFailedError} if the tx reverts or fails + */ +export async function execute( + chain: EVMChain, + opts: SetPoolParams & { wallet: unknown }, +): Promise { + const { wallet, ...params } = opts + const unsigned = await generate(chain, params) + return submit(chain, wallet, unsigned, OPERATION) +} 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 000000000..7804625da --- /dev/null +++ b/ccip-sdk/src/cct/evm/submit.test.ts @@ -0,0 +1,122 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { makeError } from 'ethers' + +import { submit } from './submit.ts' +import { + CCIPCctTxFailedError, + CCIPCctTxNotConfirmedError, + CCIPWalletInvalidError, +} from '../../errors/index.ts' +import type { EVMChain } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import { ChainFamily } from '../../networks.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() {} }, + } 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 } | null + waitError?: Error + submitError?: Error +}) { + const fail = opts.submitError + return { + signTransaction: () => (fail ? Promise.reject(fail) : Promise.resolve('0x')), + getAddress() {}, + 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 (shared CCT submit pipeline)', () => { + it('returns the txHash on a successful receipt', async () => { + const result = await submit( + stubChain(), + fakeSigner({ receipt: { status: 1 } }), + UNSIGNED, + 'setPool', + ) + assert.deepEqual(result, { txHash: HASH }) + }) + + it('throws CCIPCctTxFailedError (reverted) on status 0, tagged with the operation', async () => { + await assert.rejects( + () => submit(stubChain(), fakeSigner({ receipt: { status: 0 } }), UNSIGNED, 'setPool'), + (err: unknown) => + err instanceof CCIPCctTxFailedError && + err.context.operation === 'setPool' && + err.context.txHash === HASH && + !err.isTransient && + err.message.includes('reverted'), + ) + }) + + it('throws CCIPCctTxNotConfirmedError (transient, keeps hash) when no receipt arrives', async () => { + await assert.rejects( + () => submit(stubChain(), fakeSigner({ receipt: null }), UNSIGNED, 'setPool'), + (err: unknown) => + err instanceof CCIPCctTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, + ) + }) + + it('throws CCIPCctTxNotConfirmedError (transient, keeps hash) on confirmation timeout', async () => { + await assert.rejects( + () => + submit( + stubChain(), + fakeSigner({ waitError: makeError('timed out', 'TIMEOUT') }), + UNSIGNED, + 'setPool', + ), + (err: unknown) => + err instanceof CCIPCctTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, + ) + }) + + it('throws a transient CCIPCctTxFailedError 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 CCIPCctTxFailedError && 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 000000000..416de85bf --- /dev/null +++ b/ccip-sdk/src/cct/evm/submit.ts @@ -0,0 +1,87 @@ +/** + * Shared EVM submit pipeline for CCT ops. Distinguishes three outcomes: a + * pre-broadcast failure ({@link CCIPCctTxFailedError}, transient when the cause + * is network-related), a submitted-but-unconfirmed tx + * ({@link CCIPCctTxNotConfirmedError}, transient, keeps the hash), and a revert + * ({@link CCIPCctTxFailedError}). + * + * @packageDocumentation + */ + +import { type TransactionRequest, type TransactionResponse, isError } from 'ethers' + +import { + CCIPCctTxFailedError, + CCIPCctTxNotConfirmedError, + CCIPWalletInvalidError, +} from '../../errors/index.ts' +import { type EVMChain, isSigner, submitTransaction } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import type { CctTxResult } from '../token-manager.ts' + +const CONFIRM_TIMEOUT_MS = 60_000 + +/** True for ethers infra failures that are worth retrying (vs a real revert). */ +function isTransientError(error: unknown): boolean { + return ( + isError(error, 'TIMEOUT') || isError(error, 'NETWORK_ERROR') || isError(error, 'SERVER_ERROR') + ) +} + +/** + * Signs + submits a single-transaction CCT op and waits for it to mine. The + * `operation` label is carried into logs and every error's `context.operation`. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCIPCctTxNotConfirmedError} if submitted but not confirmed in time + * @throws {@link CCIPCctTxFailedError} if submission fails or the tx reverts + */ +export async function submit( + chain: EVMChain, + wallet: unknown, + unsigned: UnsignedEVMTx, + operation: string, +): Promise { + if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) + + chain.logger.debug(`${operation}: submitting...`) + + let response: TransactionResponse + try { + let tx: TransactionRequest = { ...unsigned.transactions[0]! } + tx = await wallet.populateTransaction(tx) + tx.from = undefined // some signers reject a pre-populated `from` + response = await submitTransaction(wallet, tx, chain.provider) + } catch (error) { + // Never broadcast — signing/RPC failure; retriable when network-related. + throw new CCIPCctTxFailedError( + 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 + try { + receipt = await response.wait(1, CONFIRM_TIMEOUT_MS) + } catch (error) { + // Broadcast but not confirmed in time — may still mine; keep the hash. + throw new CCIPCctTxNotConfirmedError(operation, response.hash, { + cause: error instanceof Error ? error : undefined, + }) + } + + if (!receipt) throw new CCIPCctTxNotConfirmedError(operation, response.hash) + if (receipt.status === 0) { + throw new CCIPCctTxFailedError(operation, 'transaction reverted', { + context: { txHash: response.hash }, + }) + } + + chain.logger.info(`${operation}: confirmed, tx =`, response.hash) + return { txHash: response.hash } +} diff --git a/ccip-sdk/src/cct/evm/validate.test.ts b/ccip-sdk/src/cct/evm/validate.test.ts new file mode 100644 index 000000000..98b575f06 --- /dev/null +++ b/ccip-sdk/src/cct/evm/validate.test.ts @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { validateAddress } from './validate.ts' +import { CCIPCctParamsInvalidError } from '../../errors/index.ts' + +const ADDR = '0x' + '11'.repeat(20) + +describe('validateAddress', () => { + it('accepts a valid address', () => { + assert.doesNotThrow(() => validateAddress('setPool', 'tokenAddress', ADDR)) + }) + + it('rejects a malformed address, tagged with operation + param', () => { + assert.throws( + () => validateAddress('setPool', 'tokenAddress', 'not-an-address'), + (err: unknown) => + err instanceof CCIPCctParamsInvalidError && + err.context.operation === 'setPool' && + err.context.param === 'tokenAddress', + ) + }) + + it('rejects a non-string value', () => { + assert.throws( + () => validateAddress('setPool', 'poolAddress', 123), + (err: unknown) => err instanceof CCIPCctParamsInvalidError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/validate.ts b/ccip-sdk/src/cct/evm/validate.ts new file mode 100644 index 000000000..8c52b7573 --- /dev/null +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -0,0 +1,23 @@ +/** + * Shared parameter validators for EVM CCT ops. + * + * @packageDocumentation + */ + +import { isAddress } from 'ethers' + +import { CCIPCctParamsInvalidError } from '../../errors/index.ts' + +/** + * Asserts `value` is a valid EVM address. + * @throws {@link CCIPCctParamsInvalidError} if it is not + */ +export function validateAddress(operation: string, param: string, value: unknown): void { + if (typeof value !== 'string' || !isAddress(value)) { + throw new CCIPCctParamsInvalidError( + operation, + param, + `must be a valid address, got ${String(value)}`, + ) + } +} diff --git a/ccip-sdk/src/cct/token-manager.ts b/ccip-sdk/src/cct/token-manager.ts new file mode 100644 index 000000000..09de5e008 --- /dev/null +++ b/ccip-sdk/src/cct/token-manager.ts @@ -0,0 +1,18 @@ +/** + * Cross-family CCT base — the CCT analogue of core's abstract `Chain`. + * + * @packageDocumentation + */ + +import type { Chain } from '../chain.ts' +import type { ChainFamily } from '../networks.ts' + +/** Result of any single-transaction CCT write. */ +export interface CctTxResult { + txHash: string +} + +/** Base for a chain-family CCT manager; subclasses hold the concrete `chain`. */ +export abstract class TokenManager { + abstract readonly chain: Chain +} diff --git a/ccip-sdk/src/errors/codes.ts b/ccip-sdk/src/errors/codes.ts index fab5a0a60..f5762fb24 100644 --- a/ccip-sdk/src/errors/codes.ts +++ b/ccip-sdk/src/errors/codes.ts @@ -172,6 +172,11 @@ export const CCIPErrorCode = { // Canton CANTON_API_ERROR: 'CANTON_API_ERROR', + + // CCT SDK + CCT_PARAMS_INVALID: 'CCT_PARAMS_INVALID', + CCT_TX_FAILED: 'CCT_TX_FAILED', + CCT_TX_NOT_CONFIRMED: 'CCT_TX_NOT_CONFIRMED', } as const /** Union type of all error codes. */ diff --git a/ccip-sdk/src/errors/index.ts b/ccip-sdk/src/errors/index.ts index 489798be5..4e7fb1f6a 100644 --- a/ccip-sdk/src/errors/index.ts +++ b/ccip-sdk/src/errors/index.ts @@ -83,6 +83,13 @@ export { CCIPContractNotRouterError, CCIPContractTypeInvalidError } from './spec // Specialized errors - Wallet & Signer export { CCIPWalletInvalidError, CCIPWalletNotSignerError } from './specialized.ts' +// Specialized errors - CCT +export { + CCIPCctParamsInvalidError, + CCIPCctTxFailedError, + CCIPCctTxNotConfirmedError, +} from './specialized.ts' + // Specialized errors - Execution export { CCIPExecTxNotConfirmedError, diff --git a/ccip-sdk/src/errors/recovery.ts b/ccip-sdk/src/errors/recovery.ts index 2eff04219..85ee359c4 100644 --- a/ccip-sdk/src/errors/recovery.ts +++ b/ccip-sdk/src/errors/recovery.ts @@ -195,6 +195,13 @@ export const DEFAULT_RECOVERY_HINTS: Partial> = { INTERACTIVE_REQUIRED: 'Provide the required input via CLI flags or environment variables, or remove --no-interactive to allow prompts.', + CCT_PARAMS_INVALID: + 'Check the operation parameters (addresses, selectors, amounts). See error.context for the offending field.', + CCT_TX_FAILED: + 'The CCT admin transaction failed. Ensure the caller holds the required role (token admin / pool owner) 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.', + NOT_IMPLEMENTED: 'This feature is not yet implemented.', UNKNOWN: 'An unknown error occurred. Check the error details.', diff --git a/ccip-sdk/src/errors/specialized.ts b/ccip-sdk/src/errors/specialized.ts index df39e5cee..4935dc1e3 100644 --- a/ccip-sdk/src/errors/specialized.ts +++ b/ccip-sdk/src/errors/specialized.ts @@ -2021,6 +2021,66 @@ export class CCIPWalletInvalidError extends CCIPError { } } +// CCT — Cross-Chain Token admin +// +// Generic across all CCT operations (setPool, applyChainUpdates, …). The +// specific operation is carried in `error.context.operation` so callers branch +// on `(code, operation)` rather than a per-op class — this keeps the error +// surface flat as the operation set grows. Reserve a dedicated subclass only +// for an op with genuinely distinct recovery semantics. + +/** Thrown before any RPC when a CCT operation's parameters fail validation. */ +export class CCIPCctParamsInvalidError extends CCIPError { + override readonly name = 'CCIPCctParamsInvalidError' + /** Creates a CCT params invalid error for `operation` (e.g. `'setPool'`). */ + 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 }, + }, + ) + } +} + +/** Thrown when a CCT operation's transaction reverts or fails after submission. */ +export class CCIPCctTxFailedError extends CCIPError { + override readonly name = 'CCIPCctTxFailedError' + /** Creates a CCT tx failed error for `operation` (e.g. `'setPool'`). */ + 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 CCT operation's transaction was submitted but not confirmed + * within the timeout. Transient — the tx may still mine; `context.txHash` lets + * the caller check before resubmitting. + */ +export class CCIPCctTxNotConfirmedError extends CCIPError { + override readonly name = 'CCIPCctTxNotConfirmedError' + /** Creates a CCT tx not-confirmed error for `operation` (e.g. `'setPool'`). */ + 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 }, + }, + ) + } +} + // Source Chain /** diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index 9c5435a82..883bb9700 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -155,7 +155,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 && @@ -169,7 +169,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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 000000000..ba42d60c1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1891 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.4.1) + '@types/node': + specifier: 25.7.0 + version: 25.7.0 + c8: + specifier: ^11.0.0 + version: 11.0.0 + eslint: + specifier: ^10.3.0 + version: 10.4.1 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.4.1) + eslint-plugin-import-x: + specifier: ^4.16.2 + version: 4.16.2(@typescript-eslint/utils@8.59.3(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1) + eslint-plugin-jsdoc: + specifier: ^62.9.0 + version: 62.9.0(eslint@10.4.1) + eslint-plugin-prettier: + specifier: ^5.5.5 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1))(eslint@10.4.1)(prettier@3.8.3) + eslint-plugin-tsdoc: + specifier: ^0.5.2 + version: 0.5.2(eslint@10.4.1)(typescript@6.0.3) + glob: + specifier: 13.0.6 + version: 13.0.6 + prettier: + specifier: 3.8.3 + version: 3.8.3 + typescript: + specifier: 6.0.3 + version: 6.0.3 + typescript-eslint: + specifier: 8.59.3 + version: 8.59.3(eslint@10.4.1)(typescript@6.0.3) + yaml: + specifier: 2.9.0 + version: 2.9.0 + +packages: + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@es-joy/jsdoccomment@0.86.0': + resolution: {integrity: sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@es-joy/resolve.exports@1.2.0': + resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==} + engines: {node: '>=10'} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@microsoft/tsdoc-config@0.18.1': + resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@package-json/types@0.0.12': + resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} + + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@sindresorhus/base62@1.0.0': + resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} + engines: {node: '>=18'} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@25.7.0': + resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==} + + '@typescript-eslint/eslint-plugin@8.59.3': + resolution: {integrity: sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.3 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.3': + resolution: {integrity: sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.59.3': + resolution: {integrity: sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.59.3': + resolution: {integrity: sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/tsconfig-utils@8.59.3': + resolution: {integrity: sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.3': + resolution: {integrity: sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.59.3': + resolution: {integrity: sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.61.0': + resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/typescript-estree@8.59.3': + resolution: {integrity: sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.59.3': + resolution: {integrity: sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.59.3': + resolution: {integrity: sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + c8@11.0.0: + resolution: {integrity: sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==} + engines: {node: 20 || >=22} + hasBin: true + peerDependencies: + monocart-coverage-reports: ^2 + peerDependenciesMeta: + monocart-coverage-reports: + optional: true + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + comment-parser@1.4.6: + resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==} + engines: {node: '>= 12.0.0'} + + comment-parser@1.4.7: + resolution: {integrity: sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==} + engines: {node: '>= 12.0.0'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + + eslint-plugin-import-x@4.16.2: + resolution: {integrity: sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/utils': ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + eslint-import-resolver-node: '*' + peerDependenciesMeta: + '@typescript-eslint/utils': + optional: true + eslint-import-resolver-node: + optional: true + + eslint-plugin-jsdoc@62.9.0: + resolution: {integrity: sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-tsdoc@0.5.2: + resolution: {integrity: sha512-BlvqjWZdBJDIPO/YU3zcPCF23CvjYT3gyu63yo6b609NNV3D1b6zceAREy2xnweuBoDpZcLNuPyAUq9cvx6bbQ==} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.4.1: + resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + + jsdoc-type-pratt-parser@7.2.0: + resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==} + engines: {node: '>=20.0.0'} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + object-deep-merge@2.0.1: + resolution: {integrity: sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parse-imports-exports@0.2.4: + resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + + parse-statements@1.0.11: + resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + reserved-identifiers@1.2.0: + resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} + engines: {node: '>=18'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + + test-exclude@8.0.0: + resolution: {integrity: sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==} + engines: {node: 20 || >=22} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + to-valid-identifier@1.0.0: + resolution: {integrity: sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==} + engines: {node: '>=20'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.59.3: + resolution: {integrity: sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.21.0: + resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@bcoe/v8-coverage@1.0.2': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@es-joy/jsdoccomment@0.86.0': + dependencies: + '@types/estree': 1.0.9 + '@typescript-eslint/types': 8.61.0 + comment-parser: 1.4.6 + esquery: 1.7.0 + jsdoc-type-pratt-parser: 7.2.0 + + '@es-joy/resolve.exports@1.2.0': {} + + '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1)': + dependencies: + eslint: 10.4.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.4.1)': + optionalDependencies: + eslint: 10.4.1 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@istanbuljs/schema@0.1.6': {} + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@microsoft/tsdoc-config@0.18.1': + dependencies: + '@microsoft/tsdoc': 0.16.0 + ajv: 8.18.0 + jju: 1.4.0 + resolve: 1.22.12 + + '@microsoft/tsdoc@0.16.0': {} + + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@package-json/types@0.0.12': {} + + '@pkgr/core@0.3.6': {} + + '@sindresorhus/base62@1.0.0': {} + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@25.7.0': + dependencies: + undici-types: 7.21.0 + + '@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.3(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.3 + '@typescript-eslint/type-utils': 8.59.3(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.3(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.3 + eslint: 10.4.1 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.3(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.3 + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/typescript-estree': 8.59.3(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.3 + debug: 4.4.3 + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.56.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@6.0.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.3(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.3(typescript@6.0.3) + '@typescript-eslint/types': 8.59.3 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + + '@typescript-eslint/scope-manager@8.59.3': + dependencies: + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/visitor-keys': 8.59.3 + + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/tsconfig-utils@8.59.3(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.59.3(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/typescript-estree': 8.59.3(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.3(eslint@10.4.1)(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.4.1 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.56.1': {} + + '@typescript-eslint/types@8.59.3': {} + + '@typescript-eslint/types@8.61.0': {} + + '@typescript-eslint/typescript-estree@8.56.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.56.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@6.0.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.4 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.59.3(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.3(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.3(typescript@6.0.3) + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/visitor-keys': 8.59.3 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.4 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.56.1(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@6.0.3) + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.3(eslint@10.4.1)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) + '@typescript-eslint/scope-manager': 8.59.3 + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/typescript-estree': 8.59.3(typescript@6.0.3) + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 + + '@typescript-eslint/visitor-keys@8.59.3': + dependencies: + '@typescript-eslint/types': 8.59.3 + eslint-visitor-keys: 5.0.1 + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + are-docs-informative@0.0.2: {} + + balanced-match@4.0.4: {} + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + c8@11.0.0: + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@istanbuljs/schema': 0.1.6 + find-up: 5.0.0 + foreground-child: 3.3.1 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + test-exclude: 8.0.0 + v8-to-istanbul: 9.3.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + comment-parser@1.4.6: {} + + comment-parser@1.4.7: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + emoji-regex@8.0.0: {} + + es-errors@1.3.0: {} + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.4.1): + dependencies: + eslint: 10.4.1 + + eslint-import-context@0.1.9(unrs-resolver@1.12.2): + dependencies: + get-tsconfig: 4.14.0 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.12.2 + + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.3(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1): + dependencies: + '@package-json/types': 0.0.12 + '@typescript-eslint/types': 8.61.0 + comment-parser: 1.4.7 + debug: 4.4.3 + eslint: 10.4.1 + eslint-import-context: 0.1.9(unrs-resolver@1.12.2) + is-glob: 4.0.3 + minimatch: 10.2.5 + semver: 7.8.4 + stable-hash-x: 0.2.0 + unrs-resolver: 1.12.2 + optionalDependencies: + '@typescript-eslint/utils': 8.59.3(eslint@10.4.1)(typescript@6.0.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-jsdoc@62.9.0(eslint@10.4.1): + dependencies: + '@es-joy/jsdoccomment': 0.86.0 + '@es-joy/resolve.exports': 1.2.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint: 10.4.1 + espree: 11.2.0 + esquery: 1.7.0 + html-entities: 2.6.0 + object-deep-merge: 2.0.1 + parse-imports-exports: 0.2.4 + semver: 7.8.4 + spdx-expression-parse: 4.0.0 + to-valid-identifier: 1.0.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1))(eslint@10.4.1)(prettier@3.8.3): + dependencies: + eslint: 10.4.1 + prettier: 3.8.3 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@10.4.1) + + eslint-plugin-tsdoc@0.5.2(eslint@10.4.1)(typescript@6.0.3): + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@typescript-eslint/utils': 8.56.1(eslint@10.4.1)(typescript@6.0.3) + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.4.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.2: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + has-flag@4.0.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + html-entities@2.6.0: {} + + html-escaper@2.0.2: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jju@1.4.0: {} + + jsdoc-type-pratt-parser@7.2.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lru-cache@11.5.1: {} + + make-dir@4.0.0: + dependencies: + semver: 7.8.4 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minipass@7.1.3: {} + + ms@2.1.3: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + object-deep-merge@2.0.1: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parse-imports-exports@0.2.4: + dependencies: + parse-statements: 1.0.11 + + parse-statements@1.0.11: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + + picomatch@4.0.4: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.8.3: {} + + punycode@2.3.1: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + reserved-identifiers@1.2.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + semver@7.8.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + + stable-hash-x@0.2.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + + test-exclude@8.0.0: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 13.0.6 + minimatch: 10.2.5 + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + to-valid-identifier@1.0.0: + dependencies: + '@sindresorhus/base62': 1.0.0 + reserved-identifiers: 1.2.0 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.59.3(eslint@10.4.1)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.3(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.3(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.3(eslint@10.4.1)(typescript@6.0.3) + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + undici-types@7.21.0: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + + yaml@2.9.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} From 387479d3ab6b827d04a2fbdb18ced6139c8f42c8 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa Date: Mon, 22 Jun 2026 16:08:25 +0100 Subject: [PATCH 02/87] Fix linting --- ccip-sdk/src/selectors.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ccip-sdk/src/selectors.ts b/ccip-sdk/src/selectors.ts index d710f226b..fbb087b52 100644 --- a/ccip-sdk/src/selectors.ts +++ b/ccip-sdk/src/selectors.ts @@ -461,6 +461,7 @@ const SELECTORS: Selectors = { selector: 4348158687435793198n, name: 'ethereum-mainnet-polygon-zkevm-1', network_type: 'MAINNET', + deprecated: true, family: 'EVM', }, '1111': { @@ -751,6 +752,7 @@ const SELECTORS: Selectors = { selector: 4560701533377838164n, name: 'bitcoin-mainnet-botanix', network_type: 'MAINNET', + deprecated: true, family: 'EVM', }, '3776': { From 93ea5a3ae6140f36d8865d0e11d9d179f4195ea5 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Fri, 3 Jul 2026 21:51:26 +0800 Subject: [PATCH 03/87] feat: add solana set pool cct feature --- ccip-sdk/src/cct/solana/index.test.ts | 100 ++++++++++++++++++ ccip-sdk/src/cct/solana/index.ts | 43 ++++++++ ccip-sdk/src/cct/solana/submit.ts | 25 +++++ .../cct/solana/token-admin-registry/index.ts | 33 ++++++ .../solana/token-admin-registry/registry.ts | 21 ++++ .../token-admin-registry/v1_6_2/index.ts | 1 + .../token-admin-registry/v1_6_2/set-pool.ts | 85 +++++++++++++++ ccip-sdk/src/cct/solana/utils.ts | 50 +++++++++ ccip-sdk/src/cct/solana/validate.ts | 24 +++++ ccip-sdk/src/cct/solana/versions.ts | 22 ++++ 10 files changed, 404 insertions(+) create mode 100644 ccip-sdk/src/cct/solana/index.test.ts create mode 100644 ccip-sdk/src/cct/solana/index.ts create mode 100644 ccip-sdk/src/cct/solana/submit.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/index.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/registry.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/index.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/set-pool.ts create mode 100644 ccip-sdk/src/cct/solana/utils.ts create mode 100644 ccip-sdk/src/cct/solana/validate.ts create mode 100644 ccip-sdk/src/cct/solana/versions.ts 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 000000000..6ed65f447 --- /dev/null +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -0,0 +1,100 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey } from '@solana/web3.js' + +import { SolanaTokenManager } from './index.ts' +import { CCIPCctParamsInvalidError, CCIPVersionUnsupportedError } from '../../errors/index.ts' +import type { SolanaChain } from '../../solana/index.ts' +import { CCIPVersion } from '../../types.ts' + +const KEY = PublicKey.default.toBase58() + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: () => { + throw new Error('should not RPC before validation') + }, + getLatestBlockhash: async () => ({ blockhash: KEY, lastValidBlockHeight: 0 }), + }, + } as unknown as SolanaChain +} + +describe('SolanaTokenManager (cct/solana)', () => { + it('fromChain exposes grouped TokenAdminRegistry operations', () => { + const chain = stubChain() + const cct = SolanaTokenManager.fromChain(chain) + assert.equal(cct.chain, chain) + assert.equal(cct.tokenAdminRegistry.chain, chain) + }) + + it('returns setPool instructions without calldata', async () => { + const cct = SolanaTokenManager.fromChain(stubChain()) + const unsigned = await cct.tokenAdminRegistry.generateUnsignedSetPool({ + tokenAddress: KEY, + routerAddress: KEY, + poolLookupTableAddress: KEY, + payer: KEY, + }) + + const [instruction] = unsigned.instructions + assert.ok(instruction) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction.data.toString('hex'), '771e0eb473e1a7ee03000000030407') + }) + + it('serializes unsigned Solana txs on demand', async () => { + const chain = stubChain() + const cct = SolanaTokenManager.fromChain(chain) + const unsigned = await cct.tokenAdminRegistry.generateUnsignedSetPool({ + tokenAddress: KEY, + routerAddress: KEY, + poolLookupTableAddress: KEY, + payer: KEY, + }) + + const base64 = await cct.serializeUnsignedTx(unsigned, KEY) + const hex = await cct.serializeUnsignedTx(unsigned, KEY, 'hex') + + assert.match(base64, /^[A-Za-z0-9+/]+=*$/) + assert.match(hex, /^[0-9a-f]+$/) + await assert.rejects( + () => cct.serializeUnsignedTx(unsigned, KEY, 'base32' as never), + /unsupported Solana transaction encoding: base32/, + ) + }) + + it('validates public keys before RPC', async () => { + const cct = SolanaTokenManager.fromChain(stubChain()) + await assert.rejects( + () => + cct.tokenAdminRegistry.generateUnsignedSetPool({ + tokenAddress: 'nope', + routerAddress: KEY, + poolLookupTableAddress: KEY, + payer: KEY, + }), + (err: unknown) => + err instanceof CCIPCctParamsInvalidError && + err.context.operation === 'setPool' && + err.context.param === 'tokenAddress', + ) + }) + + it('only supports exact TokenAdminRegistry versions', async () => { + const cct = SolanaTokenManager.fromChain(stubChain()) + await assert.rejects( + () => + cct.tokenAdminRegistry.generateUnsignedSetPool({ + tokenAddress: KEY, + routerAddress: KEY, + poolLookupTableAddress: KEY, + payer: KEY, + version: CCIPVersion.V2_0 as never, + }), + (err: unknown) => err instanceof CCIPVersionUnsupportedError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts new file mode 100644 index 000000000..eb07fc1d2 --- /dev/null +++ b/ccip-sdk/src/cct/solana/index.ts @@ -0,0 +1,43 @@ +/** + * Solana Cross-Chain Token (CCT) admin operations. + * + * @packageDocumentation + */ + +import type { ChainFamily } from '../../networks.ts' +import { TokenManager } from '../token-manager.ts' +import { SolanaTokenAdminRegistryClient } from './token-admin-registry/index.ts' +import { type SerializedSolanaTxEncoding, serializeUnsignedSolanaTx } from './utils.ts' +import type { SolanaChain } from '../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../solana/types.ts' + +/** CCT admin facade for Solana; grouped clients own contract/program operations. */ +export class SolanaTokenManager extends TokenManager { + readonly chain: SolanaChain + readonly tokenAdminRegistry: SolanaTokenAdminRegistryClient + + /** Creates a Solana CCT manager for an existing chain. */ + constructor(chain: SolanaChain) { + super() + this.chain = chain + this.tokenAdminRegistry = new SolanaTokenAdminRegistryClient(chain) + } + + /** Wraps an existing {@link SolanaChain}. */ + static fromChain(chain: SolanaChain): SolanaTokenManager { + return new SolanaTokenManager(chain) + } + + /** Serializes an unsigned Solana CCT tx for external signing. */ + serializeUnsignedTx( + unsigned: Pick, + payer: string, + encoding: SerializedSolanaTxEncoding = 'base64', + ): Promise { + return serializeUnsignedSolanaTx(this.chain.connection, unsigned, payer, encoding) + } +} + +export type { GenerateSetPoolParams, SetPoolParams } from './token-admin-registry/index.ts' +export type { SerializedSolanaTxEncoding } from './utils.ts' +export { SolanaCCTVersion } from './versions.ts' diff --git a/ccip-sdk/src/cct/solana/submit.ts b/ccip-sdk/src/cct/solana/submit.ts new file mode 100644 index 000000000..2d9a8cefe --- /dev/null +++ b/ccip-sdk/src/cct/solana/submit.ts @@ -0,0 +1,25 @@ +import { CCIPCctTxFailedError, CCIPWalletInvalidError } 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 type { CctTxResult } from '../token-manager.ts' + +/** Signs, simulates, sends and confirms a Solana CCT transaction set. */ +export async function submit( + chain: SolanaChain, + wallet: unknown, + unsigned: UnsignedSolanaTx, + operation: string, +): Promise { + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + try { + return { txHash: await simulateAndSendTxs(chain, wallet, unsigned) } + } catch (error) { + throw new CCIPCctTxFailedError( + operation, + error instanceof Error ? error.message : String(error), + { cause: error instanceof Error ? error : undefined }, + ) + } +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/index.ts b/ccip-sdk/src/cct/solana/token-admin-registry/index.ts new file mode 100644 index 000000000..93532b284 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/index.ts @@ -0,0 +1,33 @@ +import { getTokenAdminRegistry } from './registry.ts' +import type { GenerateSetPoolParams, SetPoolParams } from './v1_6_2/set-pool.ts' +import type { SolanaChain } from '../../../solana/index.ts' +import { resolveSolanaCCTVersion } from '../versions.ts' + +/** TokenAdminRegistry CCT operations for a Solana Router program. */ +export class SolanaTokenAdminRegistryClient { + readonly chain: SolanaChain + + /** Creates a TokenAdminRegistry client for an existing Solana chain. */ + constructor(chain: SolanaChain) { + this.chain = chain + } + + /** Builds unsigned Solana `setPool` instructions. */ + async generateUnsignedSetPool(opts: GenerateSetPoolParams) { + return getTokenAdminRegistry(resolveSolanaCCTVersion(opts.version)).setPool.generate( + this.chain, + opts, + ) + } + + /** Registers a token pool. */ + async setPool(opts: SetPoolParams & { wallet: unknown }) { + return getTokenAdminRegistry(resolveSolanaCCTVersion(opts.version)).setPool.execute( + this.chain, + opts, + ) + } +} + +export type { GenerateSetPoolParams, SetPoolParams } from './v1_6_2/set-pool.ts' +export { TOKEN_ADMIN_REGISTRY_IMPLEMENTATIONS, getTokenAdminRegistry } from './registry.ts' diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/registry.ts b/ccip-sdk/src/cct/solana/token-admin-registry/registry.ts new file mode 100644 index 000000000..b0d8cdeb2 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/registry.ts @@ -0,0 +1,21 @@ +import * as V1_6_2 from './v1_6_2/index.ts' +import { CCIPVersionUnsupportedError } from '../../../errors/index.ts' +import { SolanaCCTVersion } from '../versions.ts' + +/** TokenAdminRegistry implementations keyed by exact Solana CCT program version. */ +export const TOKEN_ADMIN_REGISTRY_IMPLEMENTATIONS = { + [SolanaCCTVersion.V1_6_2]: V1_6_2, +} as const + +/** Supported TokenAdminRegistry implementation version. */ +export type TokenAdminRegistryVersion = keyof typeof TOKEN_ADMIN_REGISTRY_IMPLEMENTATIONS + +function isSupportedVersion(version: unknown): version is TokenAdminRegistryVersion { + return typeof version === 'string' && version in TOKEN_ADMIN_REGISTRY_IMPLEMENTATIONS +} + +/** Returns the TokenAdminRegistry implementation for a version. */ +export function getTokenAdminRegistry(version: unknown) { + if (!isSupportedVersion(version)) throw new CCIPVersionUnsupportedError(String(version)) + return TOKEN_ADMIN_REGISTRY_IMPLEMENTATIONS[version] +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/index.ts b/ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/index.ts new file mode 100644 index 000000000..e639a490c --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/index.ts @@ -0,0 +1 @@ +export * as setPool from './set-pool.ts' diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/set-pool.ts b/ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/set-pool.ts new file mode 100644 index 000000000..e7cdf05cf --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/set-pool.ts @@ -0,0 +1,85 @@ +import { Buffer } from 'buffer' + +import { Program } from '@coral-xyz/anchor' +import { PublicKey } from '@solana/web3.js' + +import { CCIPWalletInvalidError } from '../../../../errors/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { IDL as CCIP_ROUTER_IDL } from '../../../../solana/idl/1.6.0/CCIP_ROUTER.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { type UnsignedSolanaTx, isWallet } from '../../../../solana/types.ts' +import { simulationProvider } from '../../../../solana/utils.ts' +import type { CctTxResult } from '../../../token-manager.ts' +import { submit } from '../../submit.ts' +import { derivePda } from '../../utils.ts' +import { validatePublicKey } from '../../validate.ts' +import { type SolanaCCTVersionHint, SOLANA_CCT_VERSION } from '../../versions.ts' + +export const OPERATION = 'setPool' + +/** Parameters for Solana TokenAdminRegistry `setPool`. */ +export type SetPoolParams = SolanaCCTVersionHint & { + tokenAddress: string + routerAddress: string + poolLookupTableAddress: string +} + +/** Parameters for unsigned Solana TokenAdminRegistry `setPool` generation. */ +export type GenerateSetPoolParams = SetPoolParams & { + payer: string + authority?: string +} + +function validate(params: GenerateSetPoolParams): void { + validatePublicKey(OPERATION, 'tokenAddress', params.tokenAddress) + validatePublicKey(OPERATION, 'routerAddress', params.routerAddress) + validatePublicKey(OPERATION, 'poolLookupTableAddress', params.poolLookupTableAddress) + validatePublicKey(OPERATION, 'payer', params.payer) + if (params.authority) validatePublicKey(OPERATION, 'authority', params.authority) +} + +/** Builds the unsigned Solana `setPool` instruction set. */ +export async function generate( + chain: SolanaChain, + opts: GenerateSetPoolParams, +): Promise { + validate(opts) + + const router = new PublicKey(opts.routerAddress) + const tokenMint = new PublicKey(opts.tokenAddress) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + const lookupTable = new PublicKey(opts.poolLookupTableAddress) + + const routerProgram = new Program(CCIP_ROUTER_IDL, router, simulationProvider(chain, payer)) + const config = derivePda('config', router) + const tokenAdminRegistry = derivePda('token_admin_registry', router, [tokenMint.toBuffer()]) + + const instruction = await routerProgram.methods + .setPool(Buffer.from([3, 4, 7])) + .accounts({ + config, + tokenAdminRegistry, + mint: tokenMint, + poolLookuptable: lookupTable, + authority, + }) + .instruction() + + chain.logger.debug( + `${OPERATION}: version = ${SOLANA_CCT_VERSION}, router = ${router.toBase58()}, token = ${tokenMint.toBase58()}, lookupTable = ${lookupTable.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } +} + +/** Builds and submits Solana `setPool` with `opts.wallet`. */ +export async function execute( + chain: SolanaChain, + opts: SetPoolParams & { wallet: unknown }, +): Promise { + const { wallet, ...params } = opts + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + const payer = wallet.publicKey.toBase58() + const unsigned = await generate(chain, { ...params, payer }) + return submit(chain, wallet, unsigned, OPERATION) +} diff --git a/ccip-sdk/src/cct/solana/utils.ts b/ccip-sdk/src/cct/solana/utils.ts new file mode 100644 index 000000000..e31637b54 --- /dev/null +++ b/ccip-sdk/src/cct/solana/utils.ts @@ -0,0 +1,50 @@ +import { + type Connection, + PublicKey, + TransactionMessage, + VersionedTransaction, +} from '@solana/web3.js' +import bs58 from 'bs58' + +import { CCIPCctParamsInvalidError } from '../../errors/index.ts' +import type { UnsignedSolanaTx } from '../../solana/types.ts' + +/** Supported serialized transaction encodings. */ +export type SerializedSolanaTxEncoding = 'base58' | 'base64' | 'hex' + +/** Derives a PDA from a UTF-8 seed and optional raw seed buffers. */ +export function derivePda(seed: string, programId: PublicKey, extra: Buffer[] = []): PublicKey { + return PublicKey.findProgramAddressSync([Buffer.from(seed), ...extra], programId)[0] +} + +/** Serializes an unsigned Solana tx into one unsigned v0 transaction. */ +export function serializeUnsignedSolanaTx( + connection: Connection, + unsigned: Pick, + payer: PublicKey | string, + encoding?: SerializedSolanaTxEncoding, +): Promise +export async function serializeUnsignedSolanaTx( + connection: Connection, + unsigned: Pick, + payer: PublicKey | string, + encoding = 'base64', +): Promise { + const payerKey = typeof payer === 'string' ? new PublicKey(payer) : payer + const { blockhash } = await connection.getLatestBlockhash() + const message = new TransactionMessage({ + payerKey, + recentBlockhash: blockhash, + instructions: unsigned.instructions, + }).compileToV0Message(unsigned.lookupTables) + const serialized = Buffer.from(new VersionedTransaction(message).serialize()) + + if (encoding === 'base58') return bs58.encode(serialized) + if (encoding === 'base64') return serialized.toString('base64') + if (encoding === 'hex') return serialized.toString('hex') + throw new CCIPCctParamsInvalidError( + 'serializeUnsignedTx', + 'encoding', + `unsupported Solana transaction encoding: ${String(encoding)}`, + ) +} diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts new file mode 100644 index 000000000..51a10af77 --- /dev/null +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -0,0 +1,24 @@ +import { PublicKey } from '@solana/web3.js' + +import { CCIPCctParamsInvalidError } from '../../errors/index.ts' + +/** Asserts `value` is a valid Solana public key. */ +export function validatePublicKey(operation: string, param: string, value: unknown): void { + if (typeof value !== 'string') { + throw new CCIPCctParamsInvalidError( + operation, + param, + `must be a valid Solana public key, got ${String(value)}`, + ) + } + + try { + new PublicKey(value) + } catch { + throw new CCIPCctParamsInvalidError( + operation, + param, + `must be a valid Solana public key, got ${String(value)}`, + ) + } +} diff --git a/ccip-sdk/src/cct/solana/versions.ts b/ccip-sdk/src/cct/solana/versions.ts new file mode 100644 index 000000000..4eef54689 --- /dev/null +++ b/ccip-sdk/src/cct/solana/versions.ts @@ -0,0 +1,22 @@ +/** Solana CCT program implementation versions. */ +export const SolanaCCTVersion = { + V1_6_2: '1.6.2', +} as const + +/** Supported Solana CCT program version value. */ +export type SolanaCCTVersionValue = (typeof SolanaCCTVersion)[keyof typeof SolanaCCTVersion] + +/** Default Solana CCT program version. */ +export const SOLANA_CCT_VERSION = SolanaCCTVersion.V1_6_2 + +/** Optional version hint accepted by Solana CCT operations. */ +export type SolanaCCTVersionHint = { + version?: SolanaCCTVersionValue +} + +/** Resolves a Solana CCT version hint to the default when omitted. */ +export function resolveSolanaCCTVersion( + version: unknown = SOLANA_CCT_VERSION, +): SolanaCCTVersionValue { + return version as SolanaCCTVersionValue +} From 0fecb63b8ce16ae3f1831729f0633a8d8e386d77 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Tue, 7 Jul 2026 13:23:29 +0800 Subject: [PATCH 04/87] fix: add test files --- ccip-sdk/package.json | 4 ++ ccip-sdk/src/cct/solana/index.test.ts | 60 ++-------------- ccip-sdk/src/cct/solana/index.ts | 2 +- ccip-sdk/src/cct/solana/programs/router.ts | 22 ++++++ .../__tests__/index.test.ts | 41 +++++++++++ .../__tests__/set-pool.test.ts | 70 +++++++++++++++++++ .../token-admin-registry/v1_6_2/set-pool.ts | 19 ++--- ccip-sdk/src/cct/solana/utils.test.ts | 63 +++++++++++++++++ ccip-sdk/src/cct/solana/utils.ts | 38 +++++----- ccip-sdk/src/cct/solana/validate.test.ts | 33 +++++++++ ccip-sdk/src/cct/solana/versions.ts | 4 +- 11 files changed, 272 insertions(+), 84 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/programs/router.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/__tests__/set-pool.test.ts create mode 100644 ccip-sdk/src/cct/solana/utils.test.ts create mode 100644 ccip-sdk/src/cct/solana/validate.test.ts diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index f3c970088..4c0448f34 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -31,6 +31,10 @@ "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" + }, "./src/*": "./src/*" }, "scripts": { diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 6ed65f447..612be917b 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -4,9 +4,7 @@ import { describe, it } from 'node:test' import { PublicKey } from '@solana/web3.js' import { SolanaTokenManager } from './index.ts' -import { CCIPCctParamsInvalidError, CCIPVersionUnsupportedError } from '../../errors/index.ts' import type { SolanaChain } from '../../solana/index.ts' -import { CCIPVersion } from '../../types.ts' const KEY = PublicKey.default.toBase58() @@ -14,9 +12,7 @@ function stubChain(): SolanaChain { return { logger: { debug() {}, info() {}, warn() {}, error() {} }, connection: { - getAccountInfo: () => { - throw new Error('should not RPC before validation') - }, + getAccountInfo: () => assert.fail('should not RPC before validation'), getLatestBlockhash: async () => ({ blockhash: KEY, lastValidBlockHeight: 0 }), }, } as unknown as SolanaChain @@ -30,24 +26,8 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(cct.tokenAdminRegistry.chain, chain) }) - it('returns setPool instructions without calldata', async () => { - const cct = SolanaTokenManager.fromChain(stubChain()) - const unsigned = await cct.tokenAdminRegistry.generateUnsignedSetPool({ - tokenAddress: KEY, - routerAddress: KEY, - poolLookupTableAddress: KEY, - payer: KEY, - }) - - const [instruction] = unsigned.instructions - assert.ok(instruction) - assert.equal(unsigned.mainIndex, 0) - assert.equal(instruction.data.toString('hex'), '771e0eb473e1a7ee03000000030407') - }) - it('serializes unsigned Solana txs on demand', async () => { - const chain = stubChain() - const cct = SolanaTokenManager.fromChain(chain) + const cct = SolanaTokenManager.fromChain(stubChain()) const unsigned = await cct.tokenAdminRegistry.generateUnsignedSetPool({ tokenAddress: KEY, routerAddress: KEY, @@ -55,46 +35,14 @@ describe('SolanaTokenManager (cct/solana)', () => { payer: KEY, }) - const base64 = await cct.serializeUnsignedTx(unsigned, KEY) + const base58 = await cct.serializeUnsignedTx(unsigned, KEY) const hex = await cct.serializeUnsignedTx(unsigned, KEY, 'hex') - assert.match(base64, /^[A-Za-z0-9+/]+=*$/) + assert.match(base58, /^[1-9A-HJ-NP-Za-km-z]+$/) assert.match(hex, /^[0-9a-f]+$/) await assert.rejects( () => cct.serializeUnsignedTx(unsigned, KEY, 'base32' as never), /unsupported Solana transaction encoding: base32/, ) }) - - it('validates public keys before RPC', async () => { - const cct = SolanaTokenManager.fromChain(stubChain()) - await assert.rejects( - () => - cct.tokenAdminRegistry.generateUnsignedSetPool({ - tokenAddress: 'nope', - routerAddress: KEY, - poolLookupTableAddress: KEY, - payer: KEY, - }), - (err: unknown) => - err instanceof CCIPCctParamsInvalidError && - err.context.operation === 'setPool' && - err.context.param === 'tokenAddress', - ) - }) - - it('only supports exact TokenAdminRegistry versions', async () => { - const cct = SolanaTokenManager.fromChain(stubChain()) - await assert.rejects( - () => - cct.tokenAdminRegistry.generateUnsignedSetPool({ - tokenAddress: KEY, - routerAddress: KEY, - poolLookupTableAddress: KEY, - payer: KEY, - version: CCIPVersion.V2_0 as never, - }), - (err: unknown) => err instanceof CCIPVersionUnsupportedError, - ) - }) }) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index eb07fc1d2..c00169cc3 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -32,7 +32,7 @@ export class SolanaTokenManager extends TokenManager serializeUnsignedTx( unsigned: Pick, payer: string, - encoding: SerializedSolanaTxEncoding = 'base64', + encoding?: SerializedSolanaTxEncoding, ): Promise { return serializeUnsignedSolanaTx(this.chain.connection, unsigned, payer, encoding) } 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 000000000..62323be02 --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/router.ts @@ -0,0 +1,22 @@ +import { Program } from '@coral-xyz/anchor' +import type { 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' +import { derivePda } from '../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 derivePda('config', router) +} + +/** Derives the Router token admin registry PDA for a mint. */ +export function deriveTokenAdminRegistryPda(router: PublicKey, mint: PublicKey): PublicKey { + return derivePda('token_admin_registry', router, [mint.toBuffer()]) +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts new file mode 100644 index 000000000..b23c83941 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts @@ -0,0 +1,41 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey } from '@solana/web3.js' + +import type { SolanaChain } from '../../../../solana/index.ts' +import { SolanaTokenAdminRegistryClient } from '../index.ts' + +const KEY = PublicKey.default.toBase58() + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: () => assert.fail('should not RPC before validation'), + getLatestBlockhash: async () => ({ blockhash: KEY, lastValidBlockHeight: 0 }), + }, + } as unknown as SolanaChain +} + +describe('SolanaTokenAdminRegistryClient', () => { + it('wraps an existing Solana chain', () => { + const chain = stubChain() + const client = new SolanaTokenAdminRegistryClient(chain) + + assert.equal(client.chain, chain) + }) + + it('exposes TokenAdminRegistry operations', async () => { + const client = new SolanaTokenAdminRegistryClient(stubChain()) + const unsigned = await client.generateUnsignedSetPool({ + tokenAddress: KEY, + routerAddress: KEY, + poolLookupTableAddress: KEY, + payer: KEY, + }) + + assert.equal(unsigned.instructions.length, 1) + assert.equal(unsigned.mainIndex, 0) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/set-pool.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/set-pool.test.ts new file mode 100644 index 000000000..4e7da16ba --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/set-pool.test.ts @@ -0,0 +1,70 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey } from '@solana/web3.js' + +import { CCIPCctParamsInvalidError, CCIPVersionUnsupportedError } from '../../../../errors/index.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCIPVersion } from '../../../../types.ts' +import { SolanaTokenManager } from '../../index.ts' + +const KEY = PublicKey.default.toBase58() + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: () => assert.fail('should not RPC before validation'), + getLatestBlockhash: async () => ({ blockhash: KEY, lastValidBlockHeight: 0 }), + }, + } as unknown as SolanaChain +} + +describe('Solana TokenAdminRegistry setPool', () => { + it('builds unsigned setPool instruction', async () => { + const cct = SolanaTokenManager.fromChain(stubChain()) + const unsigned = await cct.tokenAdminRegistry.generateUnsignedSetPool({ + tokenAddress: KEY, + routerAddress: KEY, + poolLookupTableAddress: KEY, + payer: KEY, + }) + + const [instruction] = unsigned.instructions + assert.ok(instruction) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction.data.toString('hex'), '771e0eb473e1a7ee03000000030407') + }) + + it('validates public keys before RPC', async () => { + const cct = SolanaTokenManager.fromChain(stubChain()) + await assert.rejects( + () => + cct.tokenAdminRegistry.generateUnsignedSetPool({ + tokenAddress: 'nope', + routerAddress: KEY, + poolLookupTableAddress: KEY, + payer: KEY, + }), + (err: unknown) => + err instanceof CCIPCctParamsInvalidError && + err.context.operation === 'setPool' && + err.context.param === 'tokenAddress', + ) + }) + + it('only supports exact TokenAdminRegistry versions', async () => { + const cct = SolanaTokenManager.fromChain(stubChain()) + await assert.rejects( + () => + cct.tokenAdminRegistry.generateUnsignedSetPool({ + tokenAddress: KEY, + routerAddress: KEY, + poolLookupTableAddress: KEY, + payer: KEY, + version: CCIPVersion.V2_0 as never, + }), + (err: unknown) => err instanceof CCIPVersionUnsupportedError, + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/set-pool.ts b/ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/set-pool.ts index e7cdf05cf..91ad5450c 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/set-pool.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/v1_6_2/set-pool.ts @@ -1,19 +1,20 @@ import { Buffer } from 'buffer' -import { Program } from '@coral-xyz/anchor' import { PublicKey } from '@solana/web3.js' import { CCIPWalletInvalidError } from '../../../../errors/index.ts' import { ChainFamily } from '../../../../networks.ts' -import { IDL as CCIP_ROUTER_IDL } from '../../../../solana/idl/1.6.0/CCIP_ROUTER.ts' import type { SolanaChain } from '../../../../solana/index.ts' import { type UnsignedSolanaTx, isWallet } from '../../../../solana/types.ts' -import { simulationProvider } from '../../../../solana/utils.ts' import type { CctTxResult } from '../../../token-manager.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' import { submit } from '../../submit.ts' -import { derivePda } from '../../utils.ts' import { validatePublicKey } from '../../validate.ts' -import { type SolanaCCTVersionHint, SOLANA_CCT_VERSION } from '../../versions.ts' +import { type SolanaCCTVersionHint, DEFAULT_SOLANA_CCT_VERSION } from '../../versions.ts' export const OPERATION = 'setPool' @@ -51,9 +52,9 @@ export async function generate( const authority = new PublicKey(opts.authority ?? opts.payer) const lookupTable = new PublicKey(opts.poolLookupTableAddress) - const routerProgram = new Program(CCIP_ROUTER_IDL, router, simulationProvider(chain, payer)) - const config = derivePda('config', router) - const tokenAdminRegistry = derivePda('token_admin_registry', router, [tokenMint.toBuffer()]) + const routerProgram = createRouterProgram(chain, router, payer) + const config = deriveRouterConfigPda(router) + const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, tokenMint) const instruction = await routerProgram.methods .setPool(Buffer.from([3, 4, 7])) @@ -67,7 +68,7 @@ export async function generate( .instruction() chain.logger.debug( - `${OPERATION}: version = ${SOLANA_CCT_VERSION}, router = ${router.toBase58()}, token = ${tokenMint.toBase58()}, lookupTable = ${lookupTable.toBase58()}`, + `${OPERATION}: version = ${DEFAULT_SOLANA_CCT_VERSION}, 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/utils.test.ts b/ccip-sdk/src/cct/solana/utils.test.ts new file mode 100644 index 000000000..668b5f7c4 --- /dev/null +++ b/ccip-sdk/src/cct/solana/utils.test.ts @@ -0,0 +1,63 @@ +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 { derivePda, serializeUnsignedSolanaTx } from './utils.ts' +import { CCIPCctParamsInvalidError } from '../../errors/index.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('cct/solana utils', () => { + it('derives PDAs from string and raw seeds', () => { + assert.equal(derivePda('config', KEY).toBase58(), derivePda('config', KEY).toBase58()) + assert.notEqual( + derivePda('config', KEY).toBase58(), + derivePda('config', KEY, [KEY.toBuffer()]).toBase58(), + ) + }) + + 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 CCIPCctParamsInvalidError && + err.context.operation === 'serializeUnsignedTx' && + err.context.param === 'lookupTables', + ) + }) + + it('rejects unsupported transaction encodings', async () => { + await assert.rejects( + () => serializeUnsignedSolanaTx(connection, unsigned, KEY, 'base32' as never), + (err: unknown) => + err instanceof CCIPCctParamsInvalidError && + err.context.operation === 'serializeUnsignedTx' && + err.context.param === 'encoding', + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/utils.ts b/ccip-sdk/src/cct/solana/utils.ts index e31637b54..a79f09502 100644 --- a/ccip-sdk/src/cct/solana/utils.ts +++ b/ccip-sdk/src/cct/solana/utils.ts @@ -1,9 +1,4 @@ -import { - type Connection, - PublicKey, - TransactionMessage, - VersionedTransaction, -} from '@solana/web3.js' +import { PublicKey, TransactionMessage } from '@solana/web3.js' import bs58 from 'bs58' import { CCIPCctParamsInvalidError } from '../../errors/index.ts' @@ -17,27 +12,38 @@ export function derivePda(seed: string, programId: PublicKey, extra: Buffer[] = return PublicKey.findProgramAddressSync([Buffer.from(seed), ...extra], programId)[0] } -/** Serializes an unsigned Solana tx into one unsigned v0 transaction. */ +/** Serializes an unsigned Solana tx into one legacy message for external signing. */ export function serializeUnsignedSolanaTx( - connection: Connection, + connection: { getLatestBlockhash: () => Promise<{ blockhash: string }> }, unsigned: Pick, payer: PublicKey | string, encoding?: SerializedSolanaTxEncoding, ): Promise export async function serializeUnsignedSolanaTx( - connection: Connection, + connection: { getLatestBlockhash: () => Promise<{ blockhash: string }> }, unsigned: Pick, payer: PublicKey | string, - encoding = 'base64', + encoding = 'base58', ): Promise { + if (unsigned.lookupTables?.length) { + throw new CCIPCctParamsInvalidError( + '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 message = new TransactionMessage({ - payerKey, - recentBlockhash: blockhash, - instructions: unsigned.instructions, - }).compileToV0Message(unsigned.lookupTables) - const serialized = Buffer.from(new VersionedTransaction(message).serialize()) + 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') 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 000000000..94c235936 --- /dev/null +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey } from '@solana/web3.js' + +import { validatePublicKey } from './validate.ts' +import { CCIPCctParamsInvalidError } from '../../errors/index.ts' + +describe('cct/solana validate', () => { + it('accepts valid public keys', () => { + assert.doesNotThrow(() => validatePublicKey('op', 'payer', PublicKey.default.toBase58())) + }) + + it('rejects non-string public keys', () => { + assert.throws( + () => validatePublicKey('op', 'payer', 123), + (err: unknown) => + err instanceof CCIPCctParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'payer', + ) + }) + + it('rejects invalid public key strings', () => { + assert.throws( + () => validatePublicKey('op', 'payer', 'nope'), + (err: unknown) => + err instanceof CCIPCctParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'payer', + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/versions.ts b/ccip-sdk/src/cct/solana/versions.ts index 4eef54689..c3285957f 100644 --- a/ccip-sdk/src/cct/solana/versions.ts +++ b/ccip-sdk/src/cct/solana/versions.ts @@ -7,7 +7,7 @@ export const SolanaCCTVersion = { export type SolanaCCTVersionValue = (typeof SolanaCCTVersion)[keyof typeof SolanaCCTVersion] /** Default Solana CCT program version. */ -export const SOLANA_CCT_VERSION = SolanaCCTVersion.V1_6_2 +export const DEFAULT_SOLANA_CCT_VERSION = SolanaCCTVersion.V1_6_2 /** Optional version hint accepted by Solana CCT operations. */ export type SolanaCCTVersionHint = { @@ -16,7 +16,7 @@ export type SolanaCCTVersionHint = { /** Resolves a Solana CCT version hint to the default when omitted. */ export function resolveSolanaCCTVersion( - version: unknown = SOLANA_CCT_VERSION, + version: unknown = DEFAULT_SOLANA_CCT_VERSION, ): SolanaCCTVersionValue { return version as SolanaCCTVersionValue } From b91ffc60e1eced6dda3380ef0f530851d4007c9f Mon Sep 17 00:00:00 2001 From: Pedro Barbosa Date: Mon, 6 Jul 2026 15:25:04 +0100 Subject: [PATCH 05/87] Better separation of concerns and abstractions --- ccip-sdk/src/cct/errors.ts | 67 +++++++++++++++ ccip-sdk/src/cct/evm/index.test.ts | 6 +- ccip-sdk/src/cct/evm/index.ts | 25 +++--- ccip-sdk/src/cct/evm/operation.ts | 36 ++++++++ ccip-sdk/src/cct/evm/operations/set-pool.ts | 84 ------------------- ccip-sdk/src/cct/evm/submit.test.ts | 27 +++--- ccip-sdk/src/cct/evm/submit.ts | 52 +++++------- .../evm/token-admin/operations/set-pool.ts | 45 ++++++++++ ccip-sdk/src/cct/evm/validate.test.ts | 30 ------- ccip-sdk/src/cct/evm/validate.ts | 6 +- ccip-sdk/src/cct/operation.ts | 26 ++++++ ccip-sdk/src/cct/token-manager.ts | 14 ++-- ccip-sdk/src/errors/codes.ts | 2 +- ccip-sdk/src/errors/index.ts | 7 -- ccip-sdk/src/errors/recovery.ts | 15 ++-- ccip-sdk/src/errors/specialized.ts | 60 ------------- 16 files changed, 243 insertions(+), 259 deletions(-) create mode 100644 ccip-sdk/src/cct/errors.ts create mode 100644 ccip-sdk/src/cct/evm/operation.ts delete mode 100644 ccip-sdk/src/cct/evm/operations/set-pool.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin/operations/set-pool.ts delete mode 100644 ccip-sdk/src/cct/evm/validate.test.ts create mode 100644 ccip-sdk/src/cct/operation.ts diff --git a/ccip-sdk/src/cct/errors.ts b/ccip-sdk/src/cct/errors.ts new file mode 100644 index 000000000..1c2f32a49 --- /dev/null +++ b/ccip-sdk/src/cct/errors.ts @@ -0,0 +1,67 @@ +/** + * 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. */ +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 or the transaction reverts after mining. + * Pre-broadcast failures (signing/RPC) may set `isTransient: true` for network errors; + * on-chain reverts are permanent. Reverts include `context.txHash`. + */ +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. + */ +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 }, + }, + ) + } +} diff --git a/ccip-sdk/src/cct/evm/index.test.ts b/ccip-sdk/src/cct/evm/index.test.ts index 1375ae9ac..fc331699a 100644 --- a/ccip-sdk/src/cct/evm/index.test.ts +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -4,9 +4,10 @@ import { describe, it } from 'node:test' import { Interface, id } from 'ethers' import { EVMTokenManager } from './index.ts' -import { CCIPCctParamsInvalidError, CCIPWalletInvalidError } from '../../errors/index.ts' +import { 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) @@ -105,7 +106,7 @@ describe('EVMTokenManager (cct/evm)', () => { routerAddress: ROUTER, }), (err: unknown) => - err instanceof CCIPCctParamsInvalidError && + err instanceof CCTParamsInvalidError && err.context.operation === 'setPool' && err.context.param === 'tokenAddress', ) @@ -128,4 +129,5 @@ describe('EVMTokenManager (cct/evm)', () => { ) }) }) + }) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index dcd1af4c4..3564881f8 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -1,5 +1,5 @@ /** - * EVM Cross-Chain Token (CCT) admin operations on the TokenAdminRegistry. + * EVM Cross-Chain Token (CCT) admin operations. * {@link EVMTokenManager} wraps an {@link EVMChain}: build with * `generateUnsigned` (sender in opts), then `` with `wallet` in opts. * @@ -12,12 +12,14 @@ 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 { TransactionHash } from '../operation.ts' import { TokenManager } from '../token-manager.ts' -import * as SetPool from './operations/set-pool.ts' +import { type SetPoolParams, SetPool } from './token-admin/operations/set-pool.ts' -/** CCT admin operations for EVM chains, delegating each op to `./operations`. */ +/** CCT admin operations for EVM chains, delegating each op to an operation class. */ export class EVMTokenManager extends TokenManager { readonly chain: EVMChain + readonly #setPool = new SetPool() /** Wraps the chain this manager builds and submits through. */ constructor(chain: EVMChain) { @@ -50,21 +52,20 @@ export class EVMTokenManager extends TokenManager { /** * Builds an unsigned `setPool` tx (for multisig / offline signing). - * @throws {@link CCIPCctParamsInvalidError} if any param is invalid + * @throws {@link CCTParamsInvalidError} if any param is invalid */ - generateUnsignedSetPool( - opts: SetPool.SetPoolParams & { sender?: string }, - ): Promise { - return SetPool.generate(this.chain, opts) + generateUnsignedSetPool(opts: SetPoolParams): Promise { + return this.#setPool.generate(this.chain, opts) } /** * Registers a pool, signing + submitting with `opts.wallet` (the token admin). * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer - * @throws {@link CCIPCctParamsInvalidError} if any param is invalid - * @throws {@link CCIPCctTxFailedError} if the tx reverts or fails + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @throws {@link CCTTxFailedError} if the tx reverts or fails */ - setPool(opts: SetPool.SetPoolParams & { wallet: unknown }): Promise { - return SetPool.execute(this.chain, opts) + setPool(opts: SetPoolParams & { wallet: unknown }): Promise { + return this.#setPool.execute(this.chain, opts) } + } diff --git a/ccip-sdk/src/cct/evm/operation.ts b/ccip-sdk/src/cct/evm/operation.ts new file mode 100644 index 000000000..c7907df24 --- /dev/null +++ b/ccip-sdk/src/cct/evm/operation.ts @@ -0,0 +1,36 @@ +/** + * EVM {@link Operation} lifecycle: validate → encode → submit. + * Concrete ops implement {@link EVMOperation.encode}; this base wires + * {@link generate} and {@link execute}. + * + * @packageDocumentation + */ + +import type { EVMChain } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import type { TransactionHash } from '../operation.ts' +import { Operation } from '../operation.ts' +import { submit } from './submit.ts' + +/** EVM CCT write base. Subclasses supply {@link validate} and {@link encode}. */ +export abstract class EVMOperation

extends Operation< + EVMChain, + P, + UnsignedEVMTx +> { + /** Encode calldata into an unsigned tx; versioned ops resolve their encoder here. */ + protected abstract encode(chain: EVMChain, params: P): Promise | UnsignedEVMTx + + /** Run {@link validate} and {@link encode}, applying optional `sender`; no signing. */ + async generate(chain: EVMChain, params: P): Promise { + this.validate(params) + const unsigned = await this.encode(chain, params) + if (params.sender && unsigned.transactions[0]) unsigned.transactions[0].from = params.sender + return unsigned + } + + /** {@link generate}, then sign and submit via {@link submit}; returns once confirmed. */ + async execute(chain: EVMChain, params: P & { wallet: unknown }): Promise { + return submit(chain, params.wallet, await this.generate(chain, params), this.name) + } +} diff --git a/ccip-sdk/src/cct/evm/operations/set-pool.ts b/ccip-sdk/src/cct/evm/operations/set-pool.ts deleted file mode 100644 index 22a9af8c2..000000000 --- a/ccip-sdk/src/cct/evm/operations/set-pool.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * `setPool` — registers a pool for a token in the TokenAdminRegistry. - * Version-independent (v1.5/v1.6/v2.0 share one encoding). - * - * @packageDocumentation - */ - -import { type TransactionRequest, Interface } from 'ethers' - -import TokenAdminRegistryABI from '../../../evm/abi/TokenAdminRegistry_1_5.ts' -import type { EVMChain } from '../../../evm/index.ts' -import type { UnsignedEVMTx } from '../../../evm/types.ts' -import { ChainFamily } from '../../../networks.ts' -import type { CctTxResult } from '../../token-manager.ts' -import { submit } from '../submit.ts' -import { validateAddress } from '../validate.ts' - -export const OPERATION = 'setPool' - -/** Parameters for `setPool`. */ -export type SetPoolParams = { - tokenAddress: string - /** Pool to register; zero address delists the token. */ - poolAddress: string - /** Router — used to discover the TokenAdminRegistry. */ - routerAddress: string -} - -/** Result of `setPool`. */ -export type SetPoolResult = CctTxResult - -/** - * Validates {@link SetPoolParams} before any RPC. - * @throws {@link CCIPCctParamsInvalidError} if any address is invalid - */ -function validate(params: SetPoolParams): void { - validateAddress(OPERATION, 'tokenAddress', params.tokenAddress) - validateAddress(OPERATION, 'poolAddress', params.poolAddress) - validateAddress(OPERATION, 'routerAddress', params.routerAddress) -} - -/** Encodes the `setPool(localToken, pool)` calldata. */ -export function encode(params: SetPoolParams): string { - return new Interface(TokenAdminRegistryABI).encodeFunctionData('setPool', [ - params.tokenAddress, - params.poolAddress, - ]) -} - -/** - * Builds an unsigned `setPool` tx on the discovered TokenAdminRegistry; set - * `sender` to populate `from`. - * @throws {@link CCIPCctParamsInvalidError} if any param is invalid - */ -export async function generate( - chain: EVMChain, - opts: SetPoolParams & { sender?: string }, -): Promise { - validate(opts) - - const to = await chain.getTokenAdminRegistryFor(opts.routerAddress) - const tx: TransactionRequest = { to, data: encode(opts) } - if (opts.sender) tx.from = opts.sender - - chain.logger.debug( - `${OPERATION}: TAR = ${to}, token = ${opts.tokenAddress}, pool = ${opts.poolAddress}`, - ) - return { family: ChainFamily.EVM, transactions: [tx] } -} - -/** - * Builds and submits `setPool` with `opts.wallet` (the token admin). - * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer - * @throws {@link CCIPCctParamsInvalidError} if any param is invalid - * @throws {@link CCIPCctTxFailedError} if the tx reverts or fails - */ -export async function execute( - chain: EVMChain, - opts: SetPoolParams & { wallet: unknown }, -): Promise { - const { wallet, ...params } = opts - const unsigned = await generate(chain, params) - return submit(chain, wallet, unsigned, OPERATION) -} diff --git a/ccip-sdk/src/cct/evm/submit.test.ts b/ccip-sdk/src/cct/evm/submit.test.ts index 7804625da..4bfa7d922 100644 --- a/ccip-sdk/src/cct/evm/submit.test.ts +++ b/ccip-sdk/src/cct/evm/submit.test.ts @@ -4,14 +4,11 @@ import { describe, it } from 'node:test' import { makeError } from 'ethers' import { submit } from './submit.ts' -import { - CCIPCctTxFailedError, - CCIPCctTxNotConfirmedError, - CCIPWalletInvalidError, -} from '../../errors/index.ts' +import { 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) @@ -56,21 +53,21 @@ function fakeSigner(opts: { } describe('submit (shared CCT submit pipeline)', () => { - it('returns the txHash on a successful receipt', async () => { + it('returns the hash on a successful receipt', async () => { const result = await submit( stubChain(), fakeSigner({ receipt: { status: 1 } }), UNSIGNED, 'setPool', ) - assert.deepEqual(result, { txHash: HASH }) + assert.deepEqual(result, { hash: HASH }) }) - it('throws CCIPCctTxFailedError (reverted) on status 0, tagged with the operation', async () => { + it('throws CCTTxFailedError (reverted) on status 0, tagged with the operation', async () => { await assert.rejects( () => submit(stubChain(), fakeSigner({ receipt: { status: 0 } }), UNSIGNED, 'setPool'), (err: unknown) => - err instanceof CCIPCctTxFailedError && + err instanceof CCTTxFailedError && err.context.operation === 'setPool' && err.context.txHash === HASH && !err.isTransient && @@ -78,15 +75,15 @@ describe('submit (shared CCT submit pipeline)', () => { ) }) - it('throws CCIPCctTxNotConfirmedError (transient, keeps hash) when no receipt arrives', async () => { + 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 CCIPCctTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, + err instanceof CCTTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, ) }) - it('throws CCIPCctTxNotConfirmedError (transient, keeps hash) on confirmation timeout', async () => { + it('throws CCTTxNotConfirmedError (transient, keeps hash) on confirmation timeout', async () => { await assert.rejects( () => submit( @@ -96,11 +93,11 @@ describe('submit (shared CCT submit pipeline)', () => { 'setPool', ), (err: unknown) => - err instanceof CCIPCctTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, + err instanceof CCTTxNotConfirmedError && err.context.txHash === HASH && err.isTransient, ) }) - it('throws a transient CCIPCctTxFailedError when submission fails with a network error', async () => { + it('throws a transient CCTTxFailedError when submission fails with a network error', async () => { await assert.rejects( () => submit( @@ -109,7 +106,7 @@ describe('submit (shared CCT submit pipeline)', () => { UNSIGNED, 'setPool', ), - (err: unknown) => err instanceof CCIPCctTxFailedError && err.isTransient, + (err: unknown) => err instanceof CCTTxFailedError && err.isTransient, ) }) diff --git a/ccip-sdk/src/cct/evm/submit.ts b/ccip-sdk/src/cct/evm/submit.ts index 416de85bf..9f00ecd91 100644 --- a/ccip-sdk/src/cct/evm/submit.ts +++ b/ccip-sdk/src/cct/evm/submit.ts @@ -1,27 +1,23 @@ /** - * Shared EVM submit pipeline for CCT ops. Distinguishes three outcomes: a - * pre-broadcast failure ({@link CCIPCctTxFailedError}, transient when the cause - * is network-related), a submitted-but-unconfirmed tx - * ({@link CCIPCctTxNotConfirmedError}, transient, keeps the hash), and a revert - * ({@link CCIPCctTxFailedError}). + * Shared sign-and-submit pipeline for EVM CCT operations. Maps broadcast, + * confirmation, and revert failures to {@link CCTTxFailedError} and + * {@link CCTTxNotConfirmedError}. * * @packageDocumentation */ import { type TransactionRequest, type TransactionResponse, isError } from 'ethers' -import { - CCIPCctTxFailedError, - CCIPCctTxNotConfirmedError, - CCIPWalletInvalidError, -} from '../../errors/index.ts' +import { CCIPWalletInvalidError } from '../../errors/index.ts' import { type EVMChain, isSigner, submitTransaction } from '../../evm/index.ts' import type { UnsignedEVMTx } from '../../evm/types.ts' -import type { CctTxResult } from '../token-manager.ts' +import { CCTTxFailedError, CCTTxNotConfirmedError } from '../errors.ts' +import type { TransactionHash } from '../operation.ts' +/** Max ms to wait for one confirmation before throwing {@link CCTTxNotConfirmedError}. */ const CONFIRM_TIMEOUT_MS = 60_000 -/** True for ethers infra failures that are worth retrying (vs a real revert). */ +/** 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') @@ -29,18 +25,18 @@ function isTransientError(error: unknown): boolean { } /** - * Signs + submits a single-transaction CCT op and waits for it to mine. The - * `operation` label is carried into logs and every error's `context.operation`. + * Signs and submits the first transaction in `unsigned`, then waits for one confirmation. + * `operation` labels logs and error context. * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer - * @throws {@link CCIPCctTxNotConfirmedError} if submitted but not confirmed in time - * @throws {@link CCIPCctTxFailedError} if submission fails or the tx reverts + * @throws {@link CCTTxNotConfirmedError} if submitted but not confirmed in time + * @throws {@link CCTTxFailedError} if submission fails or the tx reverts */ export async function submit( chain: EVMChain, wallet: unknown, unsigned: UnsignedEVMTx, operation: string, -): Promise { +): Promise { if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) chain.logger.debug(`${operation}: submitting...`) @@ -52,15 +48,10 @@ export async function submit( tx.from = undefined // some signers reject a pre-populated `from` response = await submitTransaction(wallet, tx, chain.provider) } catch (error) { - // Never broadcast — signing/RPC failure; retriable when network-related. - throw new CCIPCctTxFailedError( - operation, - error instanceof Error ? error.message : String(error), - { - cause: error instanceof Error ? error : undefined, - isTransient: isTransientError(error), - }, - ) + 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) @@ -69,19 +60,18 @@ export async function submit( try { receipt = await response.wait(1, CONFIRM_TIMEOUT_MS) } catch (error) { - // Broadcast but not confirmed in time — may still mine; keep the hash. - throw new CCIPCctTxNotConfirmedError(operation, response.hash, { + throw new CCTTxNotConfirmedError(operation, response.hash, { cause: error instanceof Error ? error : undefined, }) } - if (!receipt) throw new CCIPCctTxNotConfirmedError(operation, response.hash) + if (!receipt) throw new CCTTxNotConfirmedError(operation, response.hash) if (receipt.status === 0) { - throw new CCIPCctTxFailedError(operation, 'transaction reverted', { + throw new CCTTxFailedError(operation, 'transaction reverted', { context: { txHash: response.hash }, }) } chain.logger.info(`${operation}: confirmed, tx =`, response.hash) - return { txHash: response.hash } + return { hash: response.hash } } diff --git a/ccip-sdk/src/cct/evm/token-admin/operations/set-pool.ts b/ccip-sdk/src/cct/evm/token-admin/operations/set-pool.ts new file mode 100644 index 000000000..5fb41ecd6 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin/operations/set-pool.ts @@ -0,0 +1,45 @@ +/** + * setPool — registers a pool for a token in the TokenAdminRegistry. + * Version-independent (v1.5–v2.0 share one encoding). + * + * @packageDocumentation + */ + +import { Interface } from 'ethers' + +import TokenAdminRegistryABI from '../../../../evm/abi/TokenAdminRegistry_1_5.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `setPool`. Zero `poolAddress` delists the token. */ +export interface SetPoolParams { + tokenAddress: string + poolAddress: string + routerAddress: string + sender?: string +} + +/** Registers a pool for a token in the TokenAdminRegistry discovered from the router. */ +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, 'routerAddress', p.routerAddress) + } + + /** Encodes `setPool` on the TokenAdminRegistry discovered from the router. */ + protected async encode(chain: EVMChain, p: SetPoolParams): Promise { + const to = await chain.getTokenAdminRegistryFor(p.routerAddress) + const data = new Interface(TokenAdminRegistryABI).encodeFunctionData('setPool', [ + p.tokenAddress, + p.poolAddress, + ]) + return { family: ChainFamily.EVM, transactions: [{ to, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/validate.test.ts b/ccip-sdk/src/cct/evm/validate.test.ts deleted file mode 100644 index 98b575f06..000000000 --- a/ccip-sdk/src/cct/evm/validate.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import assert from 'node:assert/strict' -import { describe, it } from 'node:test' - -import { validateAddress } from './validate.ts' -import { CCIPCctParamsInvalidError } from '../../errors/index.ts' - -const ADDR = '0x' + '11'.repeat(20) - -describe('validateAddress', () => { - it('accepts a valid address', () => { - assert.doesNotThrow(() => validateAddress('setPool', 'tokenAddress', ADDR)) - }) - - it('rejects a malformed address, tagged with operation + param', () => { - assert.throws( - () => validateAddress('setPool', 'tokenAddress', 'not-an-address'), - (err: unknown) => - err instanceof CCIPCctParamsInvalidError && - err.context.operation === 'setPool' && - err.context.param === 'tokenAddress', - ) - }) - - it('rejects a non-string value', () => { - assert.throws( - () => validateAddress('setPool', 'poolAddress', 123), - (err: unknown) => err instanceof CCIPCctParamsInvalidError, - ) - }) -}) diff --git a/ccip-sdk/src/cct/evm/validate.ts b/ccip-sdk/src/cct/evm/validate.ts index 8c52b7573..b0b545d62 100644 --- a/ccip-sdk/src/cct/evm/validate.ts +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -6,15 +6,15 @@ import { isAddress } from 'ethers' -import { CCIPCctParamsInvalidError } from '../../errors/index.ts' +import { CCTParamsInvalidError } from '../errors.ts' /** * Asserts `value` is a valid EVM address. - * @throws {@link CCIPCctParamsInvalidError} if it is not + * @throws {@link CCTParamsInvalidError} if it is not */ export function validateAddress(operation: string, param: string, value: unknown): void { if (typeof value !== 'string' || !isAddress(value)) { - throw new CCIPCctParamsInvalidError( + throw new CCTParamsInvalidError( operation, param, `must be a valid address, got ${String(value)}`, diff --git a/ccip-sdk/src/cct/operation.ts b/ccip-sdk/src/cct/operation.ts new file mode 100644 index 000000000..d29e9b9dd --- /dev/null +++ b/ccip-sdk/src/cct/operation.ts @@ -0,0 +1,26 @@ +/** + * 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' + +/** Confirmed on-chain hash returned by a successful CCT write. */ +export type TransactionHash = Pick + +/** + * 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: Params & { wallet: unknown }): Promise +} diff --git a/ccip-sdk/src/cct/token-manager.ts b/ccip-sdk/src/cct/token-manager.ts index 09de5e008..9efe7b425 100644 --- a/ccip-sdk/src/cct/token-manager.ts +++ b/ccip-sdk/src/cct/token-manager.ts @@ -1,5 +1,6 @@ /** - * Cross-family CCT base — the CCT analogue of core's abstract `Chain`. + * Cross-family CCT manager base, the CCT analogue of core's {@link Chain}. + * Family-specific subclasses hold the chain and expose admin operations. * * @packageDocumentation */ @@ -7,12 +8,11 @@ import type { Chain } from '../chain.ts' import type { ChainFamily } from '../networks.ts' -/** Result of any single-transaction CCT write. */ -export interface CctTxResult { - txHash: string -} - -/** Base for a chain-family CCT manager; subclasses hold the concrete `chain`. */ +/** + * 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 d6e87ae20..fb447ecc5 100644 --- a/ccip-sdk/src/errors/codes.ts +++ b/ccip-sdk/src/errors/codes.ts @@ -180,7 +180,7 @@ export const CCIPErrorCode = { // Canton CANTON_API_ERROR: 'CANTON_API_ERROR', - // CCT SDK + // CCT (Cross-Chain Token) CCT_PARAMS_INVALID: 'CCT_PARAMS_INVALID', CCT_TX_FAILED: 'CCT_TX_FAILED', CCT_TX_NOT_CONFIRMED: 'CCT_TX_NOT_CONFIRMED', diff --git a/ccip-sdk/src/errors/index.ts b/ccip-sdk/src/errors/index.ts index 3847d2cac..f828c2ac0 100644 --- a/ccip-sdk/src/errors/index.ts +++ b/ccip-sdk/src/errors/index.ts @@ -91,13 +91,6 @@ export { CCIPContractNotRouterError, CCIPContractTypeInvalidError } from './spec // Specialized errors - Wallet & Signer export { CCIPWalletInvalidError, CCIPWalletNotSignerError } from './specialized.ts' -// Specialized errors - CCT -export { - CCIPCctParamsInvalidError, - CCIPCctTxFailedError, - CCIPCctTxNotConfirmedError, -} from './specialized.ts' - // Specialized errors - Execution export { CCIPExecTxNotConfirmedError, diff --git a/ccip-sdk/src/errors/recovery.ts b/ccip-sdk/src/errors/recovery.ts index 7d04f6de0..e7ad328c0 100644 --- a/ccip-sdk/src/errors/recovery.ts +++ b/ccip-sdk/src/errors/recovery.ts @@ -202,18 +202,19 @@ export const DEFAULT_RECOVERY_HINTS: Partial> = { INTERACTIVE_REQUIRED: 'Provide the required input via CLI flags or environment variables, or remove --no-interactive to allow prompts.', - CCT_PARAMS_INVALID: - 'Check the operation parameters (addresses, selectors, amounts). See error.context for the offending field.', - CCT_TX_FAILED: - 'The CCT admin transaction failed. Ensure the caller holds the required role (token admin / pool owner) 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.', - NOT_IMPLEMENTED: 'This feature is not yet implemented.', UNKNOWN: 'An unknown error occurred. Check the error details.', 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.', } /** Returns default recovery hint for error code, or undefined if none. */ diff --git a/ccip-sdk/src/errors/specialized.ts b/ccip-sdk/src/errors/specialized.ts index fa9a7bd70..e7a268582 100644 --- a/ccip-sdk/src/errors/specialized.ts +++ b/ccip-sdk/src/errors/specialized.ts @@ -2234,66 +2234,6 @@ export class CCIPWalletInvalidError extends CCIPError { } } -// CCT — Cross-Chain Token admin -// -// Generic across all CCT operations (setPool, applyChainUpdates, …). The -// specific operation is carried in `error.context.operation` so callers branch -// on `(code, operation)` rather than a per-op class — this keeps the error -// surface flat as the operation set grows. Reserve a dedicated subclass only -// for an op with genuinely distinct recovery semantics. - -/** Thrown before any RPC when a CCT operation's parameters fail validation. */ -export class CCIPCctParamsInvalidError extends CCIPError { - override readonly name = 'CCIPCctParamsInvalidError' - /** Creates a CCT params invalid error for `operation` (e.g. `'setPool'`). */ - 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 }, - }, - ) - } -} - -/** Thrown when a CCT operation's transaction reverts or fails after submission. */ -export class CCIPCctTxFailedError extends CCIPError { - override readonly name = 'CCIPCctTxFailedError' - /** Creates a CCT tx failed error for `operation` (e.g. `'setPool'`). */ - 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 CCT operation's transaction was submitted but not confirmed - * within the timeout. Transient — the tx may still mine; `context.txHash` lets - * the caller check before resubmitting. - */ -export class CCIPCctTxNotConfirmedError extends CCIPError { - override readonly name = 'CCIPCctTxNotConfirmedError' - /** Creates a CCT tx not-confirmed error for `operation` (e.g. `'setPool'`). */ - 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 }, - }, - ) - } -} - // Source Chain /** From 214b7cd6af8c1065a6eb20d8c831a6bd40546182 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Wed, 8 Jul 2026 18:53:10 +0800 Subject: [PATCH 06/87] fix: rename utils to serialize --- ccip-sdk/src/cct/solana/index.ts | 8 ++++---- ccip-sdk/src/cct/solana/programs/router.ts | 10 ++++++---- .../solana/{utils.test.ts => serialize.test.ts} | 14 +++----------- ccip-sdk/src/cct/solana/{utils.ts => serialize.ts} | 11 ----------- 4 files changed, 13 insertions(+), 30 deletions(-) rename ccip-sdk/src/cct/solana/{utils.test.ts => serialize.test.ts} (81%) rename ccip-sdk/src/cct/solana/{utils.ts => serialize.ts} (74%) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 3273aac1b..ccedab84a 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -5,11 +5,11 @@ */ import type { ChainFamily } from '../../networks.ts' -import { TokenManager } from '../token-manager.ts' -import { SolanaTokenAdminRegistryClient } from './token-admin-registry/index.ts' -import { type SerializedSolanaTxEncoding, serializeUnsignedSolanaTx } from './utils.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 { SolanaTokenAdminRegistryClient } from './token-admin-registry/index.ts' /** CCT admin facade for Solana; grouped clients own contract/program operations. */ export class SolanaTokenManager extends TokenManager { @@ -39,4 +39,4 @@ export class SolanaTokenManager extends TokenManager } export type { GenerateSetPoolParams, SetPoolParams } from './token-admin-registry/index.ts' -export type { SerializedSolanaTxEncoding } from './utils.ts' +export type { SerializedSolanaTxEncoding } from './serialize.ts' diff --git a/ccip-sdk/src/cct/solana/programs/router.ts b/ccip-sdk/src/cct/solana/programs/router.ts index 62323be02..a61761f60 100644 --- a/ccip-sdk/src/cct/solana/programs/router.ts +++ b/ccip-sdk/src/cct/solana/programs/router.ts @@ -1,10 +1,9 @@ import { Program } from '@coral-xyz/anchor' -import type { PublicKey } from '@solana/web3.js' +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' -import { derivePda } from '../utils.ts' /** Creates an Anchor Program client for the CCIP Router program. */ export function createRouterProgram(chain: SolanaChain, router: PublicKey, payer: PublicKey) { @@ -13,10 +12,13 @@ export function createRouterProgram(chain: SolanaChain, router: PublicKey, payer /** Derives the Router config PDA. */ export function deriveRouterConfigPda(router: PublicKey): PublicKey { - return derivePda('config', router) + 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 derivePda('token_admin_registry', router, [mint.toBuffer()]) + return PublicKey.findProgramAddressSync( + [Buffer.from('token_admin_registry'), mint.toBuffer()], + router, + )[0] } diff --git a/ccip-sdk/src/cct/solana/utils.test.ts b/ccip-sdk/src/cct/solana/serialize.test.ts similarity index 81% rename from ccip-sdk/src/cct/solana/utils.test.ts rename to ccip-sdk/src/cct/solana/serialize.test.ts index 5f9ea4edc..715c02e28 100644 --- a/ccip-sdk/src/cct/solana/utils.test.ts +++ b/ccip-sdk/src/cct/solana/serialize.test.ts @@ -4,7 +4,7 @@ import { describe, it } from 'node:test' import { Message, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js' import bs58 from 'bs58' -import { derivePda, serializeUnsignedSolanaTx } from './utils.ts' +import { serializeUnsignedSolanaTx } from './serialize.ts' import { CCTParamsInvalidError } from '../errors.ts' const KEY = PublicKey.default @@ -21,15 +21,7 @@ const unsigned = { ], } -describe('cct/solana utils', () => { - it('derives PDAs from string and raw seeds', () => { - assert.equal(derivePda('config', KEY).toBase58(), derivePda('config', KEY).toBase58()) - assert.notEqual( - derivePda('config', KEY).toBase58(), - derivePda('config', KEY, [KEY.toBuffer()]).toBase58(), - ) - }) - +describe('cct/solana serialize', () => { 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') @@ -53,7 +45,7 @@ describe('cct/solana utils', () => { it('rejects unsupported transaction encodings', async () => { await assert.rejects( - () => serializeUnsignedSolanaTx(connection, unsigned, KEY, 'base32' as never), + () => serializeUnsignedSolanaTx(connection, unsigned, KEY, 'base32'), (err: unknown) => err instanceof CCTParamsInvalidError && err.context.operation === 'serializeUnsignedTx' && diff --git a/ccip-sdk/src/cct/solana/utils.ts b/ccip-sdk/src/cct/solana/serialize.ts similarity index 74% rename from ccip-sdk/src/cct/solana/utils.ts rename to ccip-sdk/src/cct/solana/serialize.ts index cf4a63e1c..3632bb509 100644 --- a/ccip-sdk/src/cct/solana/utils.ts +++ b/ccip-sdk/src/cct/solana/serialize.ts @@ -7,18 +7,7 @@ import { CCTParamsInvalidError } from '../errors.ts' /** Supported serialized transaction encodings. */ export type SerializedSolanaTxEncoding = 'base58' | 'base64' | 'hex' -/** Derives a PDA from a UTF-8 seed and optional raw seed buffers. */ -export function derivePda(seed: string, programId: PublicKey, extra: Buffer[] = []): PublicKey { - return PublicKey.findProgramAddressSync([Buffer.from(seed), ...extra], programId)[0] -} - /** Serializes an unsigned Solana tx into one legacy message for external signing. */ -export function serializeUnsignedSolanaTx( - connection: { getLatestBlockhash: () => Promise<{ blockhash: string }> }, - unsigned: Pick, - payer: PublicKey | string, - encoding?: SerializedSolanaTxEncoding, -): Promise export async function serializeUnsignedSolanaTx( connection: { getLatestBlockhash: () => Promise<{ blockhash: string }> }, unsigned: Pick, From a7f2e11c3b4c3f2ce28b36b0dc4f53402d1ae9f3 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Wed, 8 Jul 2026 21:27:16 +0800 Subject: [PATCH 07/87] feat: add create lookup table solana --- ccip-sdk/src/cct/solana/index.ts | 9 +- ccip-sdk/src/cct/solana/operation.ts | 13 +- .../src/cct/solana/programs/fee-quoter.ts | 9 + ccip-sdk/src/cct/solana/programs/router.ts | 11 ++ .../src/cct/solana/programs/token-pool.ts | 17 ++ .../__tests__/create-lookup-table.test.ts | 58 +++++++ .../cct/solana/token-admin-registry/index.ts | 29 +++- .../operations/create-lookup-table.ts | 162 ++++++++++++++++++ 8 files changed, 299 insertions(+), 9 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/programs/fee-quoter.ts create mode 100644 ccip-sdk/src/cct/solana/programs/token-pool.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/__tests__/create-lookup-table.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.ts diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index ccedab84a..655c0cf1e 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -38,5 +38,12 @@ export class SolanaTokenManager extends TokenManager } } -export type { GenerateSetPoolParams, SetPoolParams } from './token-admin-registry/index.ts' +export type { + CreateLookupTableParams, + CreateLookupTableResult, + GenerateCreateLookupTableParams, + GenerateCreateLookupTableResult, + GenerateSetPoolParams, + SetPoolParams, +} from './token-admin-registry/index.ts' export type { SerializedSolanaTxEncoding } from './serialize.ts' diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts index b78b12115..6cef84d16 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -13,16 +13,15 @@ import { CCTTxFailedError } from '../errors.ts' import { type TransactionHash, Operation } from '../operation.ts' /** Solana CCT write base. Subclasses supply validation and encoding. */ -export abstract class SolanaOperation

extends Operation< - SolanaChain, - P, - UnsignedSolanaTx -> { +export abstract class SolanaOperation< + P extends { payer: string }, + Tx extends UnsignedSolanaTx = UnsignedSolanaTx, +> extends Operation { /** Encode instructions after params have been validated. */ - protected abstract encode(chain: SolanaChain, params: P): Promise + protected abstract encode(chain: SolanaChain, params: P): Promise /** Run {@link validate} and {@link encode}; no signing. */ - async generate(chain: SolanaChain, params: P): Promise { + async generate(chain: SolanaChain, params: P): Promise { this.validate(params) return this.encode(chain, params) } 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 000000000..4d35ed9b6 --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/fee-quoter.ts @@ -0,0 +1,9 @@ +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 index a61761f60..c1e51a467 100644 --- a/ccip-sdk/src/cct/solana/programs/router.ts +++ b/ccip-sdk/src/cct/solana/programs/router.ts @@ -22,3 +22,14 @@ export function deriveTokenAdminRegistryPda(router: PublicKey, mint: PublicKey): 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 000000000..7901f78e1 --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/token-pool.ts @@ -0,0 +1,17 @@ +import { PublicKey } from '@solana/web3.js' + +/** 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. */ +export function deriveTokenPoolSignerPda(poolProgram: PublicKey, mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('ccip_tokenpool_signer'), mint.toBuffer()], + poolProgram, + )[0] +} diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/create-lookup-table.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/create-lookup-table.test.ts new file mode 100644 index 000000000..f24a2c96a --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/create-lookup-table.test.ts @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' + +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' + +const KEY = PublicKey.default.toBase58() + +function stubChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getSlot: async () => 123, + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }), + }, + getTokenPoolConfig: async () => ({ token: KEY, router: KEY, tokenPoolProgram: KEY }), + _getRouterConfig: async () => ({ feeQuoter: PublicKey.default }), + } as unknown as SolanaChain +} + +describe('Solana TokenAdminRegistry createLookupTable', () => { + it('builds create + extend ALT instructions', async () => { + const cct = SolanaTokenManager.fromChain(stubChain()) + const unsigned = await cct.tokenAdminRegistry.generateUnsignedCreateLookupTable({ + tokenAddress: KEY, + poolProgramAddress: KEY, + payer: KEY, + }) + + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 2) + assert.equal(typeof unsigned.lookupTableAddress, 'string') + }) + + it('validates public keys before RPC', async () => { + const cct = SolanaTokenManager.fromChain({ + ...stubChain(), + connection: { getSlot: () => assert.fail('should not RPC before validation') }, + } as unknown as SolanaChain) + + await assert.rejects( + () => + cct.tokenAdminRegistry.generateUnsignedCreateLookupTable({ + tokenAddress: 'nope', + poolProgramAddress: KEY, + payer: KEY, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'createLookupTable' && + err.context.param === 'tokenAddress', + ) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/index.ts b/ccip-sdk/src/cct/solana/token-admin-registry/index.ts index b2f2146a0..9a2b4c273 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/index.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/index.ts @@ -1,11 +1,18 @@ +import { + type CreateLookupTableResult, + type GenerateCreateLookupTableParams, + type GenerateCreateLookupTableResult, + CreateLookupTable, +} from './operations/create-lookup-table.ts' import { type GenerateSetPoolParams, SetPool } from './operations/set-pool.ts' import type { SolanaChain } from '../../../solana/index.ts' import type { UnsignedSolanaTx } from '../../../solana/types.ts' import type { TransactionHash } from '../../operation.ts' -/** TokenAdminRegistry CCT operations for a Solana Router program. */ +/** Solana TokenAdminRegistry CCT operations. */ export class SolanaTokenAdminRegistryClient { readonly chain: SolanaChain + readonly #createLookupTable = new CreateLookupTable() readonly #setPool = new SetPool() /** Creates a TokenAdminRegistry client for an existing Solana chain. */ @@ -13,6 +20,20 @@ export class SolanaTokenAdminRegistryClient { this.chain = chain } + /** Builds unsigned Solana pool lookup table create+extend instructions. */ + generateUnsignedCreateLookupTable( + opts: GenerateCreateLookupTableParams, + ): Promise { + return this.#createLookupTable.generate(this.chain, opts) + } + + /** Creates and extends a Solana pool lookup table. */ + createLookupTable( + opts: GenerateCreateLookupTableParams & { wallet: unknown }, + ): Promise { + return this.#createLookupTable.execute(this.chain, opts) + } + /** Builds unsigned Solana `setPool` instructions. */ generateUnsignedSetPool(opts: GenerateSetPoolParams): Promise { return this.#setPool.generate(this.chain, opts) @@ -24,4 +45,10 @@ export class SolanaTokenAdminRegistryClient { } } +export type { + CreateLookupTableParams, + CreateLookupTableResult, + GenerateCreateLookupTableParams, + GenerateCreateLookupTableResult, +} from './operations/create-lookup-table.ts' export type { GenerateSetPoolParams, SetPoolParams } from './operations/set-pool.ts' 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 000000000..a476f887f --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.ts @@ -0,0 +1,162 @@ +import { getAssociatedTokenAddressSync } from '@solana/spl-token' +import { AddressLookupTableProgram, 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 { resolveATA, simulateAndSendTxs } from '../../../../solana/utils.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import type { TransactionHash } from '../../../operation.ts' +import { SolanaOperation } from '../../operation.ts' +import { deriveFeeBillingTokenConfigPda } from '../../programs/fee-quoter.ts' +import { + deriveExternalTokenPoolsSignerPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { deriveTokenPoolConfigPda, deriveTokenPoolSignerPda } from '../../programs/token-pool.ts' +import { validatePublicKey } from '../../validate.ts' + +const MAX_ALT_ADDRESSES = 256 +const EXTEND_CHUNK_SIZE = 30 + +/** Parameters for creating a pool lookup table for Solana `setPool`. */ +export type CreateLookupTableParams = { + tokenAddress: string + poolProgramAddress: string + additionalAddresses?: string[] +} + +/** Parameters for unsigned Solana lookup table generation. */ +export type GenerateCreateLookupTableParams = CreateLookupTableParams & { + payer: string + authority?: string +} + +/** Unsigned create lookup table result, including the derived ALT address. */ +export type GenerateCreateLookupTableResult = UnsignedSolanaTx & { + lookupTableAddress: string +} + +/** Submitted create lookup table result. */ +export type CreateLookupTableResult = TransactionHash & { + lookupTableAddress: string +} + +/** Builds and submits Solana ALT create+extend instructions for token pool setup. */ +export class CreateLookupTable extends SolanaOperation< + GenerateCreateLookupTableParams, + GenerateCreateLookupTableResult +> { + readonly name = 'createLookupTable' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateCreateLookupTableParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'poolProgramAddress', params.poolProgramAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + for (const [i, address] of (params.additionalAddresses ?? []).entries()) { + validatePublicKey(this.name, `additionalAddresses[${i}]`, address) + } + } + + /** Builds unsigned ALT create+extend instructions. */ + protected async encode( + chain: SolanaChain, + opts: GenerateCreateLookupTableParams, + ): Promise { + const poolProgram = new PublicKey(opts.poolProgramAddress) + const tokenMint = new PublicKey(opts.tokenAddress) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + const additionalAddresses = (opts.additionalAddresses ?? []).map((a) => new PublicKey(a)) + + const [createIx, lookupTableAddress] = AddressLookupTableProgram.createLookupTable({ + authority, + payer, + recentSlot: await chain.connection.getSlot(), + }) + + const { tokenProgram } = await resolveATA(chain.connection, tokenMint, authority) + 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) + + const addresses = [ + lookupTableAddress, + tokenAdminRegistry, + poolProgram, + poolConfig, + poolTokenAta, + poolSigner, + tokenProgram, + tokenMint, + feeTokenConfig, + routerPoolSigner, + ...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 = [] + 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}: router = ${router.toBase58()}, token = ${tokenMint.toBase58()}, lookupTable = ${lookupTableAddress.toBase58()}`, + ) + return { + family: ChainFamily.Solana, + instructions: [createIx, ...extendIxs], + mainIndex: 0, + lookupTableAddress: lookupTableAddress.toBase58(), + } + } + + /** Creates and extends a pool lookup table with `opts.wallet`. */ + override async execute( + chain: SolanaChain, + opts: GenerateCreateLookupTableParams & { wallet: unknown }, + ): Promise { + const { wallet } = opts + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + opts.payer = wallet.publicKey.toBase58() + const unsigned = await this.generate(chain, opts) + + try { + return { + hash: await simulateAndSendTxs(chain, wallet, unsigned), + lookupTableAddress: unsigned.lookupTableAddress, + } + } catch (error) { + throw new CCTTxFailedError( + this.name, + error instanceof Error ? error.message : String(error), + { cause: error instanceof Error ? error : undefined }, + ) + } + } +} From d3d6038cd7f0768c70a7febde332b3029ed1726b Mon Sep 17 00:00:00 2001 From: mervin-link Date: Thu, 9 Jul 2026 00:53:01 +0800 Subject: [PATCH 08/87] fix: submit to handle transient errors and add writable indexes as new param --- ccip-sdk/src/cct/solana/operation.test.ts | 70 +++++++++++++++++++ ccip-sdk/src/cct/solana/operation.ts | 48 +++++++------ ccip-sdk/src/cct/solana/programs/router.ts | 2 + ccip-sdk/src/cct/solana/serialize.test.ts | 1 + ccip-sdk/src/cct/solana/serialize.ts | 2 + ccip-sdk/src/cct/solana/submit.test.ts | 65 +++++++++++++++++ ccip-sdk/src/cct/solana/submit.ts | 64 +++++++++++++++++ .../__tests__/set-pool.test.ts | 50 ++++++++++++- .../cct/solana/token-admin-registry/index.ts | 5 +- .../operations/set-pool.ts | 22 +++--- ccip-sdk/src/cct/solana/validate.test.ts | 27 ++++++- ccip-sdk/src/cct/solana/validate.ts | 22 ++++++ 12 files changed, 343 insertions(+), 35 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/operation.test.ts create mode 100644 ccip-sdk/src/cct/solana/submit.test.ts create mode 100644 ccip-sdk/src/cct/solana/submit.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 000000000..27a36722b --- /dev/null +++ b/ccip-sdk/src/cct/solana/operation.test.ts @@ -0,0 +1,70 @@ +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 validate(params: { payer: string }): void { + this.validated = params.payer + } + + protected encode( + _chain: SolanaChain, + params: { payer: string; value: string }, + ): Promise { + this.captured = params.payer + return Promise.resolve({ family: ChainFamily.Solana, instructions: [] }) + } +} + +const chain = { logger: console, connection: {} } as unknown as SolanaChain + +describe('SolanaOperation', () => { + 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 encoding', 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 index b78b12115..921772bc1 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -8,43 +8,49 @@ import { CCIPWalletInvalidError } 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 } from '../errors.ts' import { type TransactionHash, 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 +} + +function withPayer

( + params: SolanaExecuteParams

, + payer: string, +): SolanaGenerateParams

{ + const { wallet: _wallet, ...rest } = params + return { ...rest, payer } as SolanaGenerateParams

+} /** Solana CCT write base. Subclasses supply validation and encoding. */ -export abstract class SolanaOperation

extends Operation< +export abstract class SolanaOperation

extends Operation< SolanaChain, - P, + SolanaGenerateParams

, UnsignedSolanaTx > { /** Encode instructions after params have been validated. */ - protected abstract encode(chain: SolanaChain, params: P): Promise + protected abstract encode( + chain: SolanaChain, + params: SolanaGenerateParams

, + ): Promise /** Run {@link validate} and {@link encode}; no signing. */ - async generate(chain: SolanaChain, params: P): Promise { + async generate(chain: SolanaChain, params: SolanaGenerateParams

): Promise { this.validate(params) return this.encode(chain, params) } /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ - async execute(chain: SolanaChain, params: P & { wallet: unknown }): Promise { + async execute(chain: SolanaChain, params: SolanaExecuteParams

): Promise { const { wallet } = params if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) - params.payer = wallet.publicKey.toBase58() - const unsigned = await this.generate(chain, params) - - try { - return { hash: await simulateAndSendTxs(chain, wallet, unsigned) } - } catch (error) { - throw new CCTTxFailedError( - this.name, - error instanceof Error ? error.message : String(error), - { - cause: error instanceof Error ? error : undefined, - }, - ) - } + const unsigned = await this.generate(chain, withPayer(params, wallet.publicKey.toBase58())) + return submit(chain, wallet, unsigned, this.name) } } diff --git a/ccip-sdk/src/cct/solana/programs/router.ts b/ccip-sdk/src/cct/solana/programs/router.ts index a61761f60..f7f2477ab 100644 --- a/ccip-sdk/src/cct/solana/programs/router.ts +++ b/ccip-sdk/src/cct/solana/programs/router.ts @@ -1,3 +1,5 @@ +import { Buffer } from 'buffer' + import { Program } from '@coral-xyz/anchor' import { PublicKey } from '@solana/web3.js' diff --git a/ccip-sdk/src/cct/solana/serialize.test.ts b/ccip-sdk/src/cct/solana/serialize.test.ts index 715c02e28..97d78cb77 100644 --- a/ccip-sdk/src/cct/solana/serialize.test.ts +++ b/ccip-sdk/src/cct/solana/serialize.test.ts @@ -1,3 +1,4 @@ +import { Buffer } from 'buffer' import assert from 'node:assert/strict' import { describe, it } from 'node:test' diff --git a/ccip-sdk/src/cct/solana/serialize.ts b/ccip-sdk/src/cct/solana/serialize.ts index 3632bb509..3bb811254 100644 --- a/ccip-sdk/src/cct/solana/serialize.ts +++ b/ccip-sdk/src/cct/solana/serialize.ts @@ -1,3 +1,5 @@ +import { Buffer } from 'buffer' + import { PublicKey, TransactionMessage } from '@solana/web3.js' import bs58 from 'bs58' 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 000000000..67e7698ce --- /dev/null +++ b/ccip-sdk/src/cct/solana/submit.test.ts @@ -0,0 +1,65 @@ +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('cct/solana submit error mapping', () => { + it('maps post-broadcast errors with a signature to not-confirmed', () => { + const cause = Object.assign(new Error('blockhash not found'), { 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 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 000000000..490e0f1af --- /dev/null +++ b/ccip-sdk/src/cct/solana/submit.ts @@ -0,0 +1,64 @@ +/** + * 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 { 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 { TransactionHash } from '../operation.ts' + +/** Signs, simulates, sends, and confirms a Solana CCT transaction. */ +export async function submit( + chain: SolanaChain, + wallet: unknown, + unsigned: UnsignedSolanaTx, + operation: string, +): Promise { + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + try { + return { hash: await simulateAndSendTxs(chain, wallet, unsigned) } + } 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) { + 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 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/__tests__/set-pool.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/set-pool.test.ts index 6b4753a04..571fc41bc 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/set-pool.test.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/set-pool.test.ts @@ -39,6 +39,55 @@ describe('Solana TokenAdminRegistry setPool', () => { assert.equal(instruction.data.toString('hex'), '771e0eb473e1a7ee03000000030407') }) + it('uses caller-provided writable indexes', async () => { + const cct = SolanaTokenManager.fromChain(stubChain()) + const unsigned = await cct.tokenAdminRegistry.generateUnsignedSetPool({ + tokenAddress: KEY, + address: KEY, + poolLookupTableAddress: KEY, + payer: KEY, + writableIndexes: [3, 4, 7, 9], + }) + + assert.equal(unsigned.instructions[0]!.data.toString('hex'), '771e0eb473e1a7ee0400000003040709') + }) + + it('rejects invalid writable indexes before RPC', async () => { + const cct = SolanaTokenManager.fromChain(stubChain()) + await assert.rejects( + () => + cct.tokenAdminRegistry.generateUnsignedSetPool({ + tokenAddress: KEY, + address: KEY, + poolLookupTableAddress: KEY, + payer: KEY, + writableIndexes: [3, 256], + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setPool' && + err.context.param === 'writableIndexes[1]', + ) + }) + + it('rejects empty writable indexes before RPC', async () => { + const cct = SolanaTokenManager.fromChain(stubChain()) + await assert.rejects( + () => + cct.tokenAdminRegistry.generateUnsignedSetPool({ + tokenAddress: KEY, + address: KEY, + poolLookupTableAddress: KEY, + payer: KEY, + writableIndexes: [], + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setPool' && + err.context.param === 'writableIndexes', + ) + }) + it('resolves the router from address', async () => { let requestedAddress: string | undefined const cct = SolanaTokenManager.fromChain({ @@ -85,7 +134,6 @@ describe('Solana TokenAdminRegistry setPool', () => { tokenAddress: KEY, address: KEY, poolLookupTableAddress: KEY, - payer: KEY, wallet: {}, }), (err: unknown) => err instanceof CCIPWalletInvalidError, diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/index.ts b/ccip-sdk/src/cct/solana/token-admin-registry/index.ts index b2f2146a0..51093e7ae 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/index.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/index.ts @@ -1,7 +1,8 @@ -import { type GenerateSetPoolParams, SetPool } from './operations/set-pool.ts' +import { type GenerateSetPoolParams, type SetPoolParams, SetPool } from './operations/set-pool.ts' import type { SolanaChain } from '../../../solana/index.ts' import type { UnsignedSolanaTx } from '../../../solana/types.ts' import type { TransactionHash } from '../../operation.ts' +import type { SolanaExecuteParams } from '../operation.ts' /** TokenAdminRegistry CCT operations for a Solana Router program. */ export class SolanaTokenAdminRegistryClient { @@ -19,7 +20,7 @@ export class SolanaTokenAdminRegistryClient { } /** Registers a token pool. */ - setPool(opts: GenerateSetPoolParams & { wallet: unknown }): Promise { + setPool(opts: SolanaExecuteParams): Promise { return this.#setPool.execute(this.chain, opts) } } 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 index 3556cccc0..3f1c75160 100644 --- 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 @@ -5,29 +5,31 @@ 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 { SolanaOperation } from '../../operation.ts' +import { type SolanaGenerateParams, SolanaOperation } from '../../operation.ts' import { createRouterProgram, deriveRouterConfigPda, deriveTokenAdminRegistryPda, } from '../../programs/router.ts' -import { validatePublicKey } from '../../validate.ts' +import { validatePublicKey, validateWritableIndexes } from '../../validate.ts' + +/** Standard BurnMint/LockRelease pool ALT writable positions. */ +export const DEFAULT_WRITABLE_INDEXES = [3, 4, 7] as const /** Parameters for Solana TokenAdminRegistry `setPool`. */ export type SetPoolParams = { tokenAddress: string address: string poolLookupTableAddress: string + writableIndexes?: number[] + authority?: string } /** Parameters for unsigned Solana TokenAdminRegistry `setPool` generation. */ -export type GenerateSetPoolParams = SetPoolParams & { - payer: string - authority?: string -} +export type GenerateSetPoolParams = SolanaGenerateParams /** Solana TokenAdminRegistry `setPool` operation. */ -export class SetPool extends SolanaOperation { +export class SetPool extends SolanaOperation { readonly name = 'setPool' /** Validates all public keys before any RPC. */ @@ -37,6 +39,7 @@ export class SetPool extends SolanaOperation { validatePublicKey(this.name, 'poolLookupTableAddress', params.poolLookupTableAddress) validatePublicKey(this.name, 'payer', params.payer) if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateWritableIndexes(this.name, 'writableIndexes', params.writableIndexes) } /** Builds the unsigned Solana `setPool` instruction set. */ @@ -55,8 +58,9 @@ export class SetPool extends SolanaOperation { const config = deriveRouterConfigPda(router) const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, tokenMint) + const writableIndexes = opts.writableIndexes ?? [...DEFAULT_WRITABLE_INDEXES] const instruction = await routerProgram.methods - .setPool(Buffer.from([3, 4, 7])) + .setPool(Buffer.from(writableIndexes)) .accounts({ config, tokenAdminRegistry, @@ -72,5 +76,3 @@ export class SetPool extends SolanaOperation { return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } } } - -export const setPool = new SetPool() diff --git a/ccip-sdk/src/cct/solana/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts index 9e690e14e..345c1e78f 100644 --- a/ccip-sdk/src/cct/solana/validate.test.ts +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -3,7 +3,7 @@ import { describe, it } from 'node:test' import { PublicKey } from '@solana/web3.js' -import { validatePublicKey } from './validate.ts' +import { validatePublicKey, validateWritableIndexes } from './validate.ts' import { CCTParamsInvalidError } from '../errors.ts' describe('cct/solana validate', () => { @@ -30,4 +30,29 @@ describe('cct/solana validate', () => { err.context.param === 'payer', ) }) + + 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 index 5412024ba..f5ddd3782 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -22,3 +22,25 @@ export function validatePublicKey(operation: string, param: string, value: unkno ) } } + +/** Asserts ALT writable indexes are a non-empty list of byte values when provided. */ +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()) { + if (!Number.isInteger(index) || index < 0 || index > 255) { + throw new CCTParamsInvalidError( + operation, + `${param}[${i}]`, + 'must be an integer between 0 and 255', + ) + } + } +} From ecbafc7e349d84dd2aa45faabe900739b2b064d5 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Thu, 9 Jul 2026 17:16:54 +0800 Subject: [PATCH 09/87] feat: add compute units field --- ccip-sdk/src/cct/solana/operation.ts | 7 ++++--- ccip-sdk/src/cct/solana/submit.ts | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts index 9f0d28919..f97a782ae 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -17,13 +17,14 @@ export type SolanaGenerateParams

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

= P & { wallet: unknown + computeUnits?: number } function withPayer

( params: SolanaExecuteParams

, payer: string, ): SolanaGenerateParams

{ - const { wallet: _wallet, ...rest } = params + const { wallet: _wallet, computeUnits: _computeUnits, ...rest } = params return { ...rest, payer } as SolanaGenerateParams

} @@ -43,10 +44,10 @@ export abstract class SolanaOperation< /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ async execute(chain: SolanaChain, params: SolanaExecuteParams

): Promise { - const { wallet } = params + const { wallet, computeUnits } = params if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) const unsigned = await this.generate(chain, withPayer(params, wallet.publicKey.toBase58())) - return submit(chain, wallet, unsigned, this.name) + return submit(chain, wallet, unsigned, this.name, computeUnits) } } diff --git a/ccip-sdk/src/cct/solana/submit.ts b/ccip-sdk/src/cct/solana/submit.ts index 490e0f1af..8a6ae08bc 100644 --- a/ccip-sdk/src/cct/solana/submit.ts +++ b/ccip-sdk/src/cct/solana/submit.ts @@ -20,11 +20,12 @@ export async function submit( wallet: unknown, unsigned: UnsignedSolanaTx, operation: string, + computeUnits?: number, ): Promise { if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) try { - return { hash: await simulateAndSendTxs(chain, wallet, unsigned) } + return { hash: await simulateAndSendTxs(chain, wallet, unsigned, computeUnits) } } catch (error) { throw createCCTSubmitError(operation, error) } From ac15827a45d6fd251a66c5fa8bf2d25321de354b Mon Sep 17 00:00:00 2001 From: Pedro Barbosa Date: Thu, 9 Jul 2026 14:24:59 +0100 Subject: [PATCH 10/87] Address PR comments --- .gitignore | 4 +- ccip-cli/src/index.ts | 2 +- ccip-sdk/src/api/index.ts | 2 +- ccip-sdk/src/cct/evm/index.test.ts | 11 +- ccip-sdk/src/cct/evm/index.ts | 7 +- ccip-sdk/src/cct/evm/operation.ts | 16 +- ccip-sdk/src/cct/evm/submit.test.ts | 32 +- ccip-sdk/src/cct/evm/submit.ts | 33 +- .../evm/token-admin/operations/set-pool.ts | 27 +- ccip-sdk/src/cct/evm/validate.ts | 24 +- ccip-sdk/src/evm/index.ts | 11 + pnpm-lock.yaml | 1891 ----------------- 12 files changed, 115 insertions(+), 1945 deletions(-) delete mode 100644 pnpm-lock.yaml diff --git a/.gitignore b/.gitignore index 7e32b04e0..2963edda1 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-cli/src/index.ts b/ccip-cli/src/index.ts index 7f0577b2c..2dcb08fb4 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -28,7 +28,7 @@ Error.stackTraceLimit = 50 // show more stack frames for better debugging // generate:nofail // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -const VERSION = '1.10.1-456acfd' +const VERSION = '1.10.2-fc3ed92' // generate:end const require = createRequire(import.meta.url) diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index 80d17a196..0772b0497 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -61,7 +61,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.10.1-456acfd' +export const SDK_VERSION = '1.10.2-fc3ed92' // generate:end /** SDK telemetry header name */ diff --git a/ccip-sdk/src/cct/evm/index.test.ts b/ccip-sdk/src/cct/evm/index.test.ts index fc331699a..cefda2256 100644 --- a/ccip-sdk/src/cct/evm/index.test.ts +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -46,7 +46,7 @@ describe('EVMTokenManager (cct/evm)', () => { const unsigned = await cct.generateUnsignedSetPool({ tokenAddress: TOKEN, poolAddress: POOL, - routerAddress: ROUTER, + address: ROUTER, sender: TOKEN, }) @@ -73,7 +73,7 @@ describe('EVMTokenManager (cct/evm)', () => { await cct.generateUnsignedSetPool({ tokenAddress: TOKEN, poolAddress: POOL, - routerAddress: ROUTER, + address: ROUTER, }) assert.equal(seen, ROUTER) }) @@ -83,7 +83,7 @@ describe('EVMTokenManager (cct/evm)', () => { const unsigned = await cct.generateUnsignedSetPool({ tokenAddress: TOKEN, poolAddress: POOL, - routerAddress: ROUTER, + address: ROUTER, }) assert.equal(unsigned.transactions[0]!.from, undefined) }) @@ -103,7 +103,7 @@ describe('EVMTokenManager (cct/evm)', () => { cct.generateUnsignedSetPool({ tokenAddress: 'not-an-address', poolAddress: POOL, - routerAddress: ROUTER, + address: ROUTER, }), (err: unknown) => err instanceof CCTParamsInvalidError && @@ -122,12 +122,11 @@ describe('EVMTokenManager (cct/evm)', () => { cct.setPool({ tokenAddress: TOKEN, poolAddress: POOL, - routerAddress: ROUTER, + address: ROUTER, wallet: {}, }), (err: unknown) => err instanceof CCIPWalletInvalidError, ) }) }) - }) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 3564881f8..73ab0e82c 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -52,6 +52,7 @@ export class EVMTokenManager extends TokenManager { /** * 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 */ generateUnsignedSetPool(opts: SetPoolParams): Promise { @@ -60,6 +61,7 @@ export class EVMTokenManager extends TokenManager { /** * 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 @@ -67,5 +69,8 @@ export class EVMTokenManager extends TokenManager { setPool(opts: SetPoolParams & { wallet: unknown }): Promise { return this.#setPool.execute(this.chain, opts) } - } + +export * from '../errors.ts' +export type { SetPoolParams } from './token-admin/operations/set-pool.ts' +export type { TransactionHash } from '../operation.ts' diff --git a/ccip-sdk/src/cct/evm/operation.ts b/ccip-sdk/src/cct/evm/operation.ts index c7907df24..e775ed16a 100644 --- a/ccip-sdk/src/cct/evm/operation.ts +++ b/ccip-sdk/src/cct/evm/operation.ts @@ -8,23 +8,25 @@ import type { EVMChain } from '../../evm/index.ts' import type { UnsignedEVMTx } from '../../evm/types.ts' -import type { TransactionHash } from '../operation.ts' -import { Operation } from '../operation.ts' +import { type TransactionHash, Operation } from '../operation.ts' import { submit } from './submit.ts' -/** EVM CCT write base. Subclasses supply {@link validate} and {@link encode}. */ +/** EVM CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. */ export abstract class EVMOperation

extends Operation< EVMChain, P, UnsignedEVMTx > { - /** Encode calldata into an unsigned tx; versioned ops resolve their encoder here. */ - protected abstract encode(chain: EVMChain, params: P): Promise | UnsignedEVMTx + /** 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 encode}, applying optional `sender`; no signing. */ + /** Run {@link validate} and {@link buildUnsigned}, applying optional `sender`; no signing. */ async generate(chain: EVMChain, params: P): Promise { this.validate(params) - const unsigned = await this.encode(chain, params) + const unsigned = await this.buildUnsigned(chain, params) if (params.sender && unsigned.transactions[0]) unsigned.transactions[0].from = params.sender return unsigned } diff --git a/ccip-sdk/src/cct/evm/submit.test.ts b/ccip-sdk/src/cct/evm/submit.test.ts index 4bfa7d922..bc95d9d4f 100644 --- a/ccip-sdk/src/cct/evm/submit.test.ts +++ b/ccip-sdk/src/cct/evm/submit.test.ts @@ -4,7 +4,7 @@ import { describe, it } from 'node:test' import { makeError } from 'ethers' import { submit } from './submit.ts' -import { CCIPWalletInvalidError } from '../../errors/index.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' @@ -22,6 +22,8 @@ function stubChain(): EVMChain { return { provider: {} as never, logger: { debug() {}, info() {}, warn() {}, error() {} }, + nextNonce: async () => 0, + rollbackNonce: () => {}, } as unknown as EVMChain } @@ -37,7 +39,7 @@ function fakeSigner(opts: { const fail = opts.submitError return { signTransaction: () => (fail ? Promise.reject(fail) : Promise.resolve('0x')), - getAddress() {}, + getAddress: () => Promise.resolve('0x' + '55'.repeat(20)), populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), sendTransaction: (_tx: unknown) => fail @@ -63,11 +65,17 @@ describe('submit (shared CCT submit pipeline)', () => { assert.deepEqual(result, { hash: HASH }) }) - it('throws CCTTxFailedError (reverted) on status 0, tagged with the operation', async () => { + it('throws CCIPExecTxRevertedError (non-transient) when wait() throws CALL_EXCEPTION', async () => { await assert.rejects( - () => submit(stubChain(), fakeSigner({ receipt: { status: 0 } }), UNSIGNED, 'setPool'), + () => + submit( + stubChain(), + fakeSigner({ waitError: makeError('execution reverted', 'CALL_EXCEPTION') }), + UNSIGNED, + 'setPool', + ), (err: unknown) => - err instanceof CCTTxFailedError && + err instanceof CCIPExecTxRevertedError && err.context.operation === 'setPool' && err.context.txHash === HASH && !err.isTransient && @@ -75,6 +83,20 @@ describe('submit (shared CCT submit pipeline)', () => { ) }) + 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'), diff --git a/ccip-sdk/src/cct/evm/submit.ts b/ccip-sdk/src/cct/evm/submit.ts index 9f00ecd91..4020fae88 100644 --- a/ccip-sdk/src/cct/evm/submit.ts +++ b/ccip-sdk/src/cct/evm/submit.ts @@ -1,14 +1,14 @@ /** - * Shared sign-and-submit pipeline for EVM CCT operations. Maps broadcast, - * confirmation, and revert failures to {@link CCTTxFailedError} and - * {@link CCTTxNotConfirmedError}. + * 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}. * * @packageDocumentation */ import { type TransactionRequest, type TransactionResponse, isError } from 'ethers' -import { CCIPWalletInvalidError } from '../../errors/index.ts' +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' @@ -28,8 +28,9 @@ function isTransientError(error: unknown): boolean { * Signs and submits the first transaction in `unsigned`, then waits for one confirmation. * `operation` labels logs and error context. * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer - * @throws {@link CCTTxNotConfirmedError} if submitted but not confirmed in time - * @throws {@link CCTTxFailedError} if submission fails or the tx reverts + * @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, @@ -38,16 +39,23 @@ export async function submit( operation: string, ): Promise { if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) - + const sender = await wallet.getAddress() chain.logger.debug(`${operation}: submitting...`) let response: TransactionResponse + let nonceConsumed = false try { let tx: TransactionRequest = { ...unsigned.transactions[0]! } + 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), @@ -60,17 +68,18 @@ export async function submit( 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) - if (receipt.status === 0) { - throw new CCTTxFailedError(operation, 'transaction reverted', { - context: { txHash: response.hash }, - }) - } chain.logger.info(`${operation}: confirmed, tx =`, response.hash) return { hash: response.hash } diff --git a/ccip-sdk/src/cct/evm/token-admin/operations/set-pool.ts b/ccip-sdk/src/cct/evm/token-admin/operations/set-pool.ts index 5fb41ecd6..4643d16c6 100644 --- a/ccip-sdk/src/cct/evm/token-admin/operations/set-pool.ts +++ b/ccip-sdk/src/cct/evm/token-admin/operations/set-pool.ts @@ -5,9 +5,7 @@ * @packageDocumentation */ -import { Interface } from 'ethers' - -import TokenAdminRegistryABI from '../../../../evm/abi/TokenAdminRegistry_1_5.ts' +import { interfaces } from '../../../../evm/const.ts' import type { EVMChain } from '../../../../evm/index.ts' import type { UnsignedEVMTx } from '../../../../evm/types.ts' import { ChainFamily } from '../../../../networks.ts' @@ -15,14 +13,20 @@ import { EVMOperation } from '../../operation.ts' import { validateAddress } from '../../validate.ts' /** Parameters for `setPool`. Zero `poolAddress` delists the token. */ -export interface SetPoolParams { +export type SetPoolParams = { tokenAddress: string + /** A zero/empty `poolAddress` delists the token from the registry. */ poolAddress: string - routerAddress: 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 discovered from the router. */ +/** Registers a pool for a token in the TokenAdminRegistry resolved from `address`. */ export class SetPool extends EVMOperation { readonly name = 'setPool' @@ -30,13 +34,14 @@ export class SetPool extends EVMOperation { protected validate(p: SetPoolParams): void { validateAddress(this.name, 'tokenAddress', p.tokenAddress) validateAddress(this.name, 'poolAddress', p.poolAddress) - validateAddress(this.name, 'routerAddress', p.routerAddress) + validateAddress(this.name, 'address', p.address) } - /** Encodes `setPool` on the TokenAdminRegistry discovered from the router. */ - protected async encode(chain: EVMChain, p: SetPoolParams): Promise { - const to = await chain.getTokenAdminRegistryFor(p.routerAddress) - const data = new Interface(TokenAdminRegistryABI).encodeFunctionData('setPool', [ + /** 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 = interfaces.TokenAdminRegistry.encodeFunctionData('setPool', [ p.tokenAddress, p.poolAddress, ]) diff --git a/ccip-sdk/src/cct/evm/validate.ts b/ccip-sdk/src/cct/evm/validate.ts index b0b545d62..0ececb67a 100644 --- a/ccip-sdk/src/cct/evm/validate.ts +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -6,18 +6,24 @@ import { 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. - * @throws {@link CCTParamsInvalidError} if it is not + * Asserts `value` is a valid EVM address. 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): void { - if (typeof value !== 'string' || !isAddress(value)) { - throw new CCTParamsInvalidError( - operation, - param, - `must be a valid address, got ${String(value)}`, - ) - } + 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), + }, + ) } diff --git a/ccip-sdk/src/evm/index.ts b/ccip-sdk/src/evm/index.ts index b2533c53a..c76006ff7 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -406,6 +406,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/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index ba42d60c1..000000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,1891 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - '@eslint/js': - specifier: ^10.0.1 - version: 10.0.1(eslint@10.4.1) - '@types/node': - specifier: 25.7.0 - version: 25.7.0 - c8: - specifier: ^11.0.0 - version: 11.0.0 - eslint: - specifier: ^10.3.0 - version: 10.4.1 - eslint-config-prettier: - specifier: ^10.1.8 - version: 10.1.8(eslint@10.4.1) - eslint-plugin-import-x: - specifier: ^4.16.2 - version: 4.16.2(@typescript-eslint/utils@8.59.3(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1) - eslint-plugin-jsdoc: - specifier: ^62.9.0 - version: 62.9.0(eslint@10.4.1) - eslint-plugin-prettier: - specifier: ^5.5.5 - version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1))(eslint@10.4.1)(prettier@3.8.3) - eslint-plugin-tsdoc: - specifier: ^0.5.2 - version: 0.5.2(eslint@10.4.1)(typescript@6.0.3) - glob: - specifier: 13.0.6 - version: 13.0.6 - prettier: - specifier: 3.8.3 - version: 3.8.3 - typescript: - specifier: 6.0.3 - version: 6.0.3 - typescript-eslint: - specifier: 8.59.3 - version: 8.59.3(eslint@10.4.1)(typescript@6.0.3) - yaml: - specifier: 2.9.0 - version: 2.9.0 - -packages: - - '@bcoe/v8-coverage@1.0.2': - resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} - engines: {node: '>=18'} - - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - - '@es-joy/jsdoccomment@0.86.0': - resolution: {integrity: sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@es-joy/resolve.exports@1.2.0': - resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==} - engines: {node: '>=10'} - - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.23.5': - resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/config-helpers@0.6.0': - resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/core@1.2.1': - resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/js@10.0.1': - resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^10.0.0 - peerDependenciesMeta: - eslint: - optional: true - - '@eslint/object-schema@3.0.5': - resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@eslint/plugin-kit@0.7.2': - resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - '@humanfs/core@0.19.2': - resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.8': - resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} - engines: {node: '>=18.18.0'} - - '@humanfs/types@0.15.0': - resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - - '@istanbuljs/schema@0.1.6': - resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} - engines: {node: '>=8'} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@microsoft/tsdoc-config@0.18.1': - resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==} - - '@microsoft/tsdoc@0.16.0': - resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} - - '@napi-rs/wasm-runtime@1.1.5': - resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@package-json/types@0.0.12': - resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} - - '@pkgr/core@0.3.6': - resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} - engines: {node: ^14.18.0 || >=16.0.0} - - '@sindresorhus/base62@1.0.0': - resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} - engines: {node: '>=18'} - - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - - '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/node@25.7.0': - resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==} - - '@typescript-eslint/eslint-plugin@8.59.3': - resolution: {integrity: sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.59.3 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/parser@8.59.3': - resolution: {integrity: sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/project-service@8.56.1': - resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/project-service@8.59.3': - resolution: {integrity: sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/scope-manager@8.56.1': - resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/scope-manager@8.59.3': - resolution: {integrity: sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.56.1': - resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/tsconfig-utils@8.59.3': - resolution: {integrity: sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/type-utils@8.59.3': - resolution: {integrity: sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/types@8.56.1': - resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/types@8.59.3': - resolution: {integrity: sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/types@8.61.0': - resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.56.1': - resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/typescript-estree@8.59.3': - resolution: {integrity: sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/utils@8.56.1': - resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.59.3': - resolution: {integrity: sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/visitor-keys@8.56.1': - resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.59.3': - resolution: {integrity: sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@unrs/resolver-binding-android-arm-eabi@1.12.2': - resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} - cpu: [arm] - os: [android] - - '@unrs/resolver-binding-android-arm64@1.12.2': - resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} - cpu: [arm64] - os: [android] - - '@unrs/resolver-binding-darwin-arm64@1.12.2': - resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} - cpu: [arm64] - os: [darwin] - - '@unrs/resolver-binding-darwin-x64@1.12.2': - resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} - cpu: [x64] - os: [darwin] - - '@unrs/resolver-binding-freebsd-x64@1.12.2': - resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} - cpu: [x64] - os: [freebsd] - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': - resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': - resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': - resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} - cpu: [arm64] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-musl@1.12.2': - resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} - cpu: [arm64] - os: [linux] - - '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': - resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} - cpu: [loong64] - os: [linux] - - '@unrs/resolver-binding-linux-loong64-musl@1.12.2': - resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} - cpu: [loong64] - os: [linux] - - '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': - resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} - cpu: [ppc64] - os: [linux] - - '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': - resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} - cpu: [riscv64] - os: [linux] - - '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': - resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} - cpu: [riscv64] - os: [linux] - - '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': - resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} - cpu: [s390x] - os: [linux] - - '@unrs/resolver-binding-linux-x64-gnu@1.12.2': - resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} - cpu: [x64] - os: [linux] - - '@unrs/resolver-binding-linux-x64-musl@1.12.2': - resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} - cpu: [x64] - os: [linux] - - '@unrs/resolver-binding-openharmony-arm64@1.12.2': - resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} - cpu: [arm64] - os: [openharmony] - - '@unrs/resolver-binding-wasm32-wasi@1.12.2': - resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': - resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} - cpu: [arm64] - os: [win32] - - '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': - resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} - cpu: [ia32] - os: [win32] - - '@unrs/resolver-binding-win32-x64-msvc@1.12.2': - resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} - cpu: [x64] - os: [win32] - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.15.0: - resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - are-docs-informative@0.0.2: - resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} - engines: {node: '>=14'} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} - engines: {node: 18 || 20 || >=22} - - c8@11.0.0: - resolution: {integrity: sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==} - engines: {node: 20 || >=22} - hasBin: true - peerDependencies: - monocart-coverage-reports: ^2 - peerDependenciesMeta: - monocart-coverage-reports: - optional: true - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - comment-parser@1.4.6: - resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==} - engines: {node: '>= 12.0.0'} - - comment-parser@1.4.7: - resolution: {integrity: sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==} - engines: {node: '>= 12.0.0'} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-config-prettier@10.1.8: - resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-import-context@0.1.9: - resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - peerDependencies: - unrs-resolver: ^1.0.0 - peerDependenciesMeta: - unrs-resolver: - optional: true - - eslint-plugin-import-x@4.16.2: - resolution: {integrity: sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/utils': ^8.56.0 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - eslint-import-resolver-node: '*' - peerDependenciesMeta: - '@typescript-eslint/utils': - optional: true - eslint-import-resolver-node: - optional: true - - eslint-plugin-jsdoc@62.9.0: - resolution: {integrity: sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 - - eslint-plugin-prettier@5.5.6: - resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true - - eslint-plugin-tsdoc@0.5.2: - resolution: {integrity: sha512-BlvqjWZdBJDIPO/YU3zcPCF23CvjYT3gyu63yo6b609NNV3D1b6zceAREy2xnweuBoDpZcLNuPyAUq9cvx6bbQ==} - - eslint-scope@9.1.2: - resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@5.0.1: - resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - eslint@10.4.1: - resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@11.2.0: - resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-tsconfig@4.14.0: - resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@13.0.6: - resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} - engines: {node: 18 || 20 || >=22} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - hasown@2.0.4: - resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} - engines: {node: '>= 0.4'} - - html-entities@2.6.0: - resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} - - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} - engines: {node: '>= 0.4'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - - jju@1.4.0: - resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - - jsdoc-type-pratt-parser@7.2.0: - resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==} - engines: {node: '>=20.0.0'} - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lru-cache@11.5.1: - resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} - engines: {node: 20 || >=22} - - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} - - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - - minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - napi-postinstall@0.3.4: - resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - object-deep-merge@2.0.1: - resolution: {integrity: sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - parse-imports-exports@0.2.4: - resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} - - parse-statements@1.0.11: - resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-scurry@2.0.2: - resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} - engines: {node: 18 || 20 || >=22} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier-linter-helpers@1.0.1: - resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} - engines: {node: '>=6.0.0'} - - prettier@3.8.3: - resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} - engines: {node: '>=14'} - hasBin: true - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - reserved-identifiers@1.2.0: - resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} - engines: {node: '>=18'} - - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} - engines: {node: '>= 0.4'} - hasBin: true - - semver@7.8.4: - resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} - engines: {node: '>=10'} - hasBin: true - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@4.0.0: - resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} - - spdx-license-ids@3.0.23: - resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} - - stable-hash-x@0.2.0: - resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} - engines: {node: '>=12.0.0'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - synckit@0.11.13: - resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} - engines: {node: ^14.18.0 || >=16.0.0} - - test-exclude@8.0.0: - resolution: {integrity: sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==} - engines: {node: 20 || >=22} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - to-valid-identifier@1.0.0: - resolution: {integrity: sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==} - engines: {node: '>=20'} - - ts-api-utils@2.5.0: - resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - typescript-eslint@8.59.3: - resolution: {integrity: sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@7.21.0: - resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} - - unrs-resolver@1.12.2: - resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} - engines: {node: '>= 14.6'} - hasBin: true - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - -snapshots: - - '@bcoe/v8-coverage@1.0.2': {} - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@es-joy/jsdoccomment@0.86.0': - dependencies: - '@types/estree': 1.0.9 - '@typescript-eslint/types': 8.61.0 - comment-parser: 1.4.6 - esquery: 1.7.0 - jsdoc-type-pratt-parser: 7.2.0 - - '@es-joy/resolve.exports@1.2.0': {} - - '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1)': - dependencies: - eslint: 10.4.1 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.23.5': - dependencies: - '@eslint/object-schema': 3.0.5 - debug: 4.4.3 - minimatch: 10.2.5 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.6.0': - dependencies: - '@eslint/core': 1.2.1 - - '@eslint/core@1.2.1': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/js@10.0.1(eslint@10.4.1)': - optionalDependencies: - eslint: 10.4.1 - - '@eslint/object-schema@3.0.5': {} - - '@eslint/plugin-kit@0.7.2': - dependencies: - '@eslint/core': 1.2.1 - levn: 0.4.1 - - '@humanfs/core@0.19.2': - dependencies: - '@humanfs/types': 0.15.0 - - '@humanfs/node@0.16.8': - dependencies: - '@humanfs/core': 0.19.2 - '@humanfs/types': 0.15.0 - '@humanwhocodes/retry': 0.4.3 - - '@humanfs/types@0.15.0': {} - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@istanbuljs/schema@0.1.6': {} - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@microsoft/tsdoc-config@0.18.1': - dependencies: - '@microsoft/tsdoc': 0.16.0 - ajv: 8.18.0 - jju: 1.4.0 - resolve: 1.22.12 - - '@microsoft/tsdoc@0.16.0': {} - - '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 - optional: true - - '@package-json/types@0.0.12': {} - - '@pkgr/core@0.3.6': {} - - '@sindresorhus/base62@1.0.0': {} - - '@tybys/wasm-util@0.10.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/esrecurse@4.3.1': {} - - '@types/estree@1.0.9': {} - - '@types/istanbul-lib-coverage@2.0.6': {} - - '@types/json-schema@7.0.15': {} - - '@types/node@25.7.0': - dependencies: - undici-types: 7.21.0 - - '@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1)(typescript@6.0.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.3(eslint@10.4.1)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.59.3 - '@typescript-eslint/type-utils': 8.59.3(eslint@10.4.1)(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.3(eslint@10.4.1)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.59.3 - eslint: 10.4.1 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.59.3(eslint@10.4.1)(typescript@6.0.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.59.3 - '@typescript-eslint/types': 8.59.3 - '@typescript-eslint/typescript-estree': 8.59.3(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.59.3 - debug: 4.4.3 - eslint: 10.4.1 - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.56.1(typescript@6.0.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@6.0.3) - '@typescript-eslint/types': 8.56.1 - debug: 4.4.3 - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.59.3(typescript@6.0.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.3(typescript@6.0.3) - '@typescript-eslint/types': 8.59.3 - debug: 4.4.3 - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.56.1': - dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 - - '@typescript-eslint/scope-manager@8.59.3': - dependencies: - '@typescript-eslint/types': 8.59.3 - '@typescript-eslint/visitor-keys': 8.59.3 - - '@typescript-eslint/tsconfig-utils@8.56.1(typescript@6.0.3)': - dependencies: - typescript: 6.0.3 - - '@typescript-eslint/tsconfig-utils@8.59.3(typescript@6.0.3)': - dependencies: - typescript: 6.0.3 - - '@typescript-eslint/type-utils@8.59.3(eslint@10.4.1)(typescript@6.0.3)': - dependencies: - '@typescript-eslint/types': 8.59.3 - '@typescript-eslint/typescript-estree': 8.59.3(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.3(eslint@10.4.1)(typescript@6.0.3) - debug: 4.4.3 - eslint: 10.4.1 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.56.1': {} - - '@typescript-eslint/types@8.59.3': {} - - '@typescript-eslint/types@8.61.0': {} - - '@typescript-eslint/typescript-estree@8.56.1(typescript@6.0.3)': - dependencies: - '@typescript-eslint/project-service': 8.56.1(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@6.0.3) - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.4 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@8.59.3(typescript@6.0.3)': - dependencies: - '@typescript-eslint/project-service': 8.59.3(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.59.3(typescript@6.0.3) - '@typescript-eslint/types': 8.59.3 - '@typescript-eslint/visitor-keys': 8.59.3 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.4 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.56.1(eslint@10.4.1)(typescript@6.0.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@6.0.3) - eslint: 10.4.1 - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.59.3(eslint@10.4.1)(typescript@6.0.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) - '@typescript-eslint/scope-manager': 8.59.3 - '@typescript-eslint/types': 8.59.3 - '@typescript-eslint/typescript-estree': 8.59.3(typescript@6.0.3) - eslint: 10.4.1 - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.56.1': - dependencies: - '@typescript-eslint/types': 8.56.1 - eslint-visitor-keys: 5.0.1 - - '@typescript-eslint/visitor-keys@8.59.3': - dependencies: - '@typescript-eslint/types': 8.59.3 - eslint-visitor-keys: 5.0.1 - - '@unrs/resolver-binding-android-arm-eabi@1.12.2': - optional: true - - '@unrs/resolver-binding-android-arm64@1.12.2': - optional: true - - '@unrs/resolver-binding-darwin-arm64@1.12.2': - optional: true - - '@unrs/resolver-binding-darwin-x64@1.12.2': - optional: true - - '@unrs/resolver-binding-freebsd-x64@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-arm64-musl@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-loong64-musl@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-x64-gnu@1.12.2': - optional: true - - '@unrs/resolver-binding-linux-x64-musl@1.12.2': - optional: true - - '@unrs/resolver-binding-openharmony-arm64@1.12.2': - optional: true - - '@unrs/resolver-binding-wasm32-wasi@1.12.2': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - - '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': - optional: true - - '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': - optional: true - - '@unrs/resolver-binding-win32-x64-msvc@1.12.2': - optional: true - - acorn-jsx@5.3.2(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - - acorn@8.16.0: {} - - ajv@6.15.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ajv@8.18.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - are-docs-informative@0.0.2: {} - - balanced-match@4.0.4: {} - - brace-expansion@5.0.6: - dependencies: - balanced-match: 4.0.4 - - c8@11.0.0: - dependencies: - '@bcoe/v8-coverage': 1.0.2 - '@istanbuljs/schema': 0.1.6 - find-up: 5.0.0 - foreground-child: 3.3.1 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - test-exclude: 8.0.0 - v8-to-istanbul: 9.3.0 - yargs: 17.7.2 - yargs-parser: 21.1.1 - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - comment-parser@1.4.6: {} - - comment-parser@1.4.7: {} - - convert-source-map@2.0.0: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-is@0.1.4: {} - - emoji-regex@8.0.0: {} - - es-errors@1.3.0: {} - - escalade@3.2.0: {} - - escape-string-regexp@4.0.0: {} - - eslint-config-prettier@10.1.8(eslint@10.4.1): - dependencies: - eslint: 10.4.1 - - eslint-import-context@0.1.9(unrs-resolver@1.12.2): - dependencies: - get-tsconfig: 4.14.0 - stable-hash-x: 0.2.0 - optionalDependencies: - unrs-resolver: 1.12.2 - - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.3(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1): - dependencies: - '@package-json/types': 0.0.12 - '@typescript-eslint/types': 8.61.0 - comment-parser: 1.4.7 - debug: 4.4.3 - eslint: 10.4.1 - eslint-import-context: 0.1.9(unrs-resolver@1.12.2) - is-glob: 4.0.3 - minimatch: 10.2.5 - semver: 7.8.4 - stable-hash-x: 0.2.0 - unrs-resolver: 1.12.2 - optionalDependencies: - '@typescript-eslint/utils': 8.59.3(eslint@10.4.1)(typescript@6.0.3) - transitivePeerDependencies: - - supports-color - - eslint-plugin-jsdoc@62.9.0(eslint@10.4.1): - dependencies: - '@es-joy/jsdoccomment': 0.86.0 - '@es-joy/resolve.exports': 1.2.0 - are-docs-informative: 0.0.2 - comment-parser: 1.4.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint: 10.4.1 - espree: 11.2.0 - esquery: 1.7.0 - html-entities: 2.6.0 - object-deep-merge: 2.0.1 - parse-imports-exports: 0.2.4 - semver: 7.8.4 - spdx-expression-parse: 4.0.0 - to-valid-identifier: 1.0.0 - transitivePeerDependencies: - - supports-color - - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1))(eslint@10.4.1)(prettier@3.8.3): - dependencies: - eslint: 10.4.1 - prettier: 3.8.3 - prettier-linter-helpers: 1.0.1 - synckit: 0.11.13 - optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.4.1) - - eslint-plugin-tsdoc@0.5.2(eslint@10.4.1)(typescript@6.0.3): - dependencies: - '@microsoft/tsdoc': 0.16.0 - '@microsoft/tsdoc-config': 0.18.1 - '@typescript-eslint/utils': 8.56.1(eslint@10.4.1)(typescript@6.0.3) - transitivePeerDependencies: - - eslint - - supports-color - - typescript - - eslint-scope@9.1.2: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.9 - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@5.0.1: {} - - eslint@10.4.1: - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 - '@eslint/config-helpers': 0.6.0 - '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.2 - '@humanfs/node': 0.16.8 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.9 - ajv: 6.15.0 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 9.1.2 - eslint-visitor-keys: 5.0.1 - espree: 11.2.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@11.2.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 5.0.1 - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - fast-deep-equal@3.1.3: {} - - fast-diff@1.3.0: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fast-uri@3.1.2: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.4.2 - keyv: 4.5.4 - - flatted@3.4.2: {} - - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - function-bind@1.1.2: {} - - get-caller-file@2.0.5: {} - - get-tsconfig@4.14.0: - dependencies: - resolve-pkg-maps: 1.0.0 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@13.0.6: - dependencies: - minimatch: 10.2.5 - minipass: 7.1.3 - path-scurry: 2.0.2 - - has-flag@4.0.0: {} - - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 - - html-entities@2.6.0: {} - - html-escaper@2.0.2: {} - - ignore@5.3.2: {} - - ignore@7.0.5: {} - - imurmurhash@0.1.4: {} - - is-core-module@2.16.2: - dependencies: - hasown: 2.0.4 - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - isexe@2.0.0: {} - - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - - jju@1.4.0: {} - - jsdoc-type-pratt-parser@7.2.0: {} - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-schema-traverse@1.0.0: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lru-cache@11.5.1: {} - - make-dir@4.0.0: - dependencies: - semver: 7.8.4 - - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.6 - - minipass@7.1.3: {} - - ms@2.1.3: {} - - napi-postinstall@0.3.4: {} - - natural-compare@1.4.0: {} - - object-deep-merge@2.0.1: {} - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - parse-imports-exports@0.2.4: - dependencies: - parse-statements: 1.0.11 - - parse-statements@1.0.11: {} - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-scurry@2.0.2: - dependencies: - lru-cache: 11.5.1 - minipass: 7.1.3 - - picomatch@4.0.4: {} - - prelude-ls@1.2.1: {} - - prettier-linter-helpers@1.0.1: - dependencies: - fast-diff: 1.3.0 - - prettier@3.8.3: {} - - punycode@2.3.1: {} - - require-directory@2.1.1: {} - - require-from-string@2.0.2: {} - - reserved-identifiers@1.2.0: {} - - resolve-pkg-maps@1.0.0: {} - - resolve@1.22.12: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - semver@7.8.4: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - signal-exit@4.1.0: {} - - spdx-exceptions@2.5.0: {} - - spdx-expression-parse@4.0.0: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.23 - - spdx-license-ids@3.0.23: {} - - stable-hash-x@0.2.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - synckit@0.11.13: - dependencies: - '@pkgr/core': 0.3.6 - - test-exclude@8.0.0: - dependencies: - '@istanbuljs/schema': 0.1.6 - glob: 13.0.6 - minimatch: 10.2.5 - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - to-valid-identifier@1.0.0: - dependencies: - '@sindresorhus/base62': 1.0.0 - reserved-identifiers: 1.2.0 - - ts-api-utils@2.5.0(typescript@6.0.3): - dependencies: - typescript: 6.0.3 - - tslib@2.8.1: - optional: true - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - typescript-eslint@8.59.3(eslint@10.4.1)(typescript@6.0.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.59.3(@typescript-eslint/parser@8.59.3(eslint@10.4.1)(typescript@6.0.3))(eslint@10.4.1)(typescript@6.0.3) - '@typescript-eslint/parser': 8.59.3(eslint@10.4.1)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.59.3(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.3(eslint@10.4.1)(typescript@6.0.3) - eslint: 10.4.1 - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - typescript@6.0.3: {} - - undici-types@7.21.0: {} - - unrs-resolver@1.12.2: - dependencies: - napi-postinstall: 0.3.4 - optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.12.2 - '@unrs/resolver-binding-android-arm64': 1.12.2 - '@unrs/resolver-binding-darwin-arm64': 1.12.2 - '@unrs/resolver-binding-darwin-x64': 1.12.2 - '@unrs/resolver-binding-freebsd-x64': 1.12.2 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 - '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 - '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 - '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 - '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 - '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 - '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 - '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 - '@unrs/resolver-binding-linux-x64-musl': 1.12.2 - '@unrs/resolver-binding-openharmony-arm64': 1.12.2 - '@unrs/resolver-binding-wasm32-wasi': 1.12.2 - '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 - '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 - '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - v8-to-istanbul@9.3.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - word-wrap@1.2.5: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - y18n@5.0.8: {} - - yaml@2.9.0: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yocto-queue@0.1.0: {} From fca9dc5dfa4bd262df87c76e5d4a942431901f85 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Thu, 9 Jul 2026 22:50:17 +0800 Subject: [PATCH 11/87] fix: address comments --- ccip-sdk/src/cct/solana/index.test.ts | 48 +++++++++---------- ccip-sdk/src/cct/solana/index.ts | 22 ++++++++- ccip-sdk/src/cct/solana/submit.test.ts | 13 ++++- ccip-sdk/src/cct/solana/submit.ts | 17 ++++++- .../__tests__/index.test.ts | 23 +-------- .../operations/set-pool.ts | 4 ++ ccip-sdk/src/cct/solana/validate.ts | 13 +++-- 7 files changed, 85 insertions(+), 55 deletions(-) diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 83a63a9e5..2b78481e6 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -1,21 +1,15 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' -import { PublicKey } from '@solana/web3.js' +import { Connection } from '@solana/web3.js' import { SolanaTokenManager } from './index.ts' -import type { SolanaChain } from '../../solana/index.ts' - -const KEY = PublicKey.default.toBase58() +import { SolanaChain } from '../../solana/index.ts' function stubChain(): SolanaChain { return { logger: { debug() {}, info() {}, warn() {}, error() {} }, - connection: { - getAccountInfo: () => assert.fail('should not RPC before validation'), - getLatestBlockhash: async () => ({ blockhash: KEY, lastValidBlockHeight: 0 }), - }, - getTokenAdminRegistryFor: async () => KEY, + connection: {}, } as unknown as SolanaChain } @@ -24,26 +18,32 @@ describe('SolanaTokenManager (cct/solana)', () => { const chain = stubChain() const cct = SolanaTokenManager.fromChain(chain) assert.equal(cct.chain, chain) + assert.equal(cct.provider, chain.connection) assert.equal(cct.tokenAdminRegistry.chain, chain) }) - it('serializes unsigned Solana txs on demand', async () => { - const cct = SolanaTokenManager.fromChain(stubChain()) - const unsigned = await cct.tokenAdminRegistry.generateUnsignedSetPool({ - tokenAddress: KEY, - address: KEY, - poolLookupTableAddress: KEY, - payer: KEY, + 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 base58 = await cct.serializeUnsignedTx(unsigned, KEY) - const hex = await cct.serializeUnsignedTx(unsigned, KEY, 'hex') + const cct = await SolanaTokenManager.fromUrl('http://localhost:8899') - assert.match(base58, /^[1-9A-HJ-NP-Za-km-z]+$/) - assert.match(hex, /^[0-9a-f]+$/) - await assert.rejects( - () => cct.serializeUnsignedTx(unsigned, KEY, 'base32' as never), - /unsupported Solana transaction encoding: base32/, - ) + assert.equal(cct.chain, chain) }) }) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index ccedab84a..966943b6c 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -4,8 +4,11 @@ * @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 { 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' @@ -28,13 +31,28 @@ export class SolanaTokenManager extends TokenManager return new SolanaTokenManager(chain) } + /** Creates from a Solana web3.js connection. */ + static async fromProvider(provider: Connection, ctx?: ChainContext): Promise { + return new SolanaTokenManager(await SolanaChain.fromConnection(provider, ctx)) + } + + /** Creates from an RPC URL. */ + static async fromUrl(url: string, ctx?: ChainContext): Promise { + return new SolanaTokenManager(await SolanaChain.fromUrl(url, ctx)) + } + + /** Provider of the underlying chain. */ + get provider(): Connection { + return this.chain.connection + } + /** Serializes an unsigned Solana CCT tx for external signing. */ serializeUnsignedTx( unsigned: Pick, payer: string, encoding?: SerializedSolanaTxEncoding, ): Promise { - return serializeUnsignedSolanaTx(this.chain.connection, unsigned, payer, encoding) + return serializeUnsignedSolanaTx(this.provider, unsigned, payer, encoding) } } diff --git a/ccip-sdk/src/cct/solana/submit.test.ts b/ccip-sdk/src/cct/solana/submit.test.ts index 67e7698ce..703e95155 100644 --- a/ccip-sdk/src/cct/solana/submit.test.ts +++ b/ccip-sdk/src/cct/solana/submit.test.ts @@ -9,8 +9,8 @@ import { createCCTSubmitError } from './submit.ts' const OP = 'setPool' describe('cct/solana submit error mapping', () => { - it('maps post-broadcast errors with a signature to not-confirmed', () => { - const cause = Object.assign(new Error('blockhash not found'), { signature: 'abc' }) + 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) @@ -37,6 +37,15 @@ describe('cct/solana submit error mapping', () => { 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', diff --git a/ccip-sdk/src/cct/solana/submit.ts b/ccip-sdk/src/cct/solana/submit.ts index 490e0f1af..7fab0e71b 100644 --- a/ccip-sdk/src/cct/solana/submit.ts +++ b/ccip-sdk/src/cct/solana/submit.ts @@ -7,6 +7,12 @@ * @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' @@ -36,7 +42,7 @@ export function createCCTSubmitError( error: unknown, ): CCTTxFailedError | CCTTxNotConfirmedError { const signature = getSignature(error) - if (signature) { + if (signature && isNotConfirmedError(error)) { return new CCTTxNotConfirmedError(operation, signature, { cause: error instanceof Error ? error : undefined, }) @@ -52,6 +58,15 @@ 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) } diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts index 3ca6017be..9c0dbb85c 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts @@ -1,21 +1,13 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' -import { PublicKey } from '@solana/web3.js' - import type { SolanaChain } from '../../../../solana/index.ts' import { SolanaTokenAdminRegistryClient } from '../index.ts' -const KEY = PublicKey.default.toBase58() - function stubChain(): SolanaChain { return { logger: { debug() {}, info() {}, warn() {}, error() {} }, - connection: { - getAccountInfo: () => assert.fail('should not RPC before validation'), - getLatestBlockhash: async () => ({ blockhash: KEY, lastValidBlockHeight: 0 }), - }, - getTokenAdminRegistryFor: async () => KEY, + connection: {}, } as unknown as SolanaChain } @@ -26,17 +18,4 @@ describe('SolanaTokenAdminRegistryClient', () => { assert.equal(client.chain, chain) }) - - it('exposes TokenAdminRegistry operations', async () => { - const client = new SolanaTokenAdminRegistryClient(stubChain()) - const unsigned = await client.generateUnsignedSetPool({ - tokenAddress: KEY, - address: KEY, - poolLookupTableAddress: KEY, - payer: KEY, - }) - - assert.equal(unsigned.instructions.length, 1) - assert.equal(unsigned.mainIndex, 0) - }) }) 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 index 3f1c75160..f7629f215 100644 --- 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 @@ -22,6 +22,10 @@ export type SetPoolParams = { address: string poolLookupTableAddress: string writableIndexes?: number[] + /** + * Token admin authority. Defaults to `payer` for single-signer transactions. + * Multisig/Squads flows should pass the admin/vault authority explicitly. + */ authority?: string } diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index f5ddd3782..bb4fdae66 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -1,4 +1,7 @@ -import { SolanaChain } from '../../solana/index.ts' +import { PublicKey } from '@solana/web3.js' + +import { CCIPAddressInvalidError } from '../../errors/index.ts' +import { ChainFamily } from '../../networks.ts' import { CCTParamsInvalidError } from '../errors.ts' /** Asserts `value` is a valid Solana public key string. */ @@ -12,13 +15,15 @@ export function validatePublicKey(operation: string, param: string, value: unkno } try { - SolanaChain.getAddress(value) - } catch (error) { + new PublicKey(value) + } catch { throw new CCTParamsInvalidError( operation, param, `must be a valid Solana public key, got ${String(value)}`, - { cause: error instanceof Error ? error : undefined }, + { + cause: new CCIPAddressInvalidError(value, ChainFamily.Solana), + }, ) } } From c3a41b5a1388d6dbb23ebd84fe656b9f748d3be4 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Thu, 9 Jul 2026 23:34:13 +0800 Subject: [PATCH 12/87] fix: rename encode to buildUnsigned --- ccip-sdk/src/cct/solana/operation.test.ts | 2 +- ccip-sdk/src/cct/solana/operation.ts | 12 +- .../__tests__/index.test.ts | 21 --- .../__tests__/set-pool.test.ts | 142 ------------------ .../operations/set-pool.test.ts | 86 +++++++++++ .../operations/set-pool.ts | 2 +- 6 files changed, 94 insertions(+), 171 deletions(-) delete mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts delete mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/__tests__/set-pool.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.test.ts diff --git a/ccip-sdk/src/cct/solana/operation.test.ts b/ccip-sdk/src/cct/solana/operation.test.ts index 27a36722b..363df1c53 100644 --- a/ccip-sdk/src/cct/solana/operation.test.ts +++ b/ccip-sdk/src/cct/solana/operation.test.ts @@ -18,7 +18,7 @@ class TestOperation extends SolanaOperation<{ value: string }> { this.validated = params.payer } - protected encode( + protected buildUnsigned( _chain: SolanaChain, params: { payer: string; value: string }, ): Promise { diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts index 921772bc1..7da08f406 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -1,5 +1,5 @@ /** - * Solana {@link Operation} lifecycle: validate → encode → submit. + * Solana {@link Operation} lifecycle: validate → build unsigned tx → submit. * Default execution uses wallet.publicKey as payer; use generateUnsigned* for a custom payer. * * @packageDocumentation @@ -27,22 +27,22 @@ function withPayer

( return { ...rest, payer } as SolanaGenerateParams

} -/** Solana CCT write base. Subclasses supply validation and encoding. */ +/** Solana CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. */ export abstract class SolanaOperation

extends Operation< SolanaChain, SolanaGenerateParams

, UnsignedSolanaTx > { - /** Encode instructions after params have been validated. */ - protected abstract encode( + /** Build instructions after params have been validated. */ + protected abstract buildUnsigned( chain: SolanaChain, params: SolanaGenerateParams

, ): Promise - /** Run {@link validate} and {@link encode}; no signing. */ + /** Run {@link validate} and {@link buildUnsigned}; no signing. */ async generate(chain: SolanaChain, params: SolanaGenerateParams

): Promise { this.validate(params) - return this.encode(chain, params) + return this.buildUnsigned(chain, params) } /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts deleted file mode 100644 index 9c0dbb85c..000000000 --- a/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/index.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import assert from 'node:assert/strict' -import { describe, it } from 'node:test' - -import type { SolanaChain } from '../../../../solana/index.ts' -import { SolanaTokenAdminRegistryClient } from '../index.ts' - -function stubChain(): SolanaChain { - return { - logger: { debug() {}, info() {}, warn() {}, error() {} }, - connection: {}, - } as unknown as SolanaChain -} - -describe('SolanaTokenAdminRegistryClient', () => { - it('wraps an existing Solana chain', () => { - const chain = stubChain() - const client = new SolanaTokenAdminRegistryClient(chain) - - assert.equal(client.chain, chain) - }) -}) diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/set-pool.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/set-pool.test.ts deleted file mode 100644 index 571fc41bc..000000000 --- a/ccip-sdk/src/cct/solana/token-admin-registry/__tests__/set-pool.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -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 type { SolanaChain } from '../../../../solana/index.ts' -import { CCTParamsInvalidError } from '../../../errors.ts' -import { SolanaTokenManager } from '../../index.ts' - -const KEY = PublicKey.default.toBase58() -const ADDRESS = Keypair.generate().publicKey.toBase58() -const ROUTER = Keypair.generate().publicKey.toBase58() - -function stubChain(): SolanaChain { - return { - logger: { debug() {}, info() {}, warn() {}, error() {} }, - connection: { - getAccountInfo: () => assert.fail('should not RPC before validation'), - getLatestBlockhash: async () => ({ blockhash: KEY, lastValidBlockHeight: 0 }), - }, - getTokenAdminRegistryFor: async () => KEY, - } as unknown as SolanaChain -} - -describe('Solana TokenAdminRegistry setPool', () => { - it('builds unsigned setPool instruction', async () => { - const cct = SolanaTokenManager.fromChain(stubChain()) - const unsigned = await cct.tokenAdminRegistry.generateUnsignedSetPool({ - tokenAddress: KEY, - address: KEY, - poolLookupTableAddress: KEY, - payer: KEY, - }) - - const [instruction] = unsigned.instructions - assert.ok(instruction) - assert.equal(unsigned.mainIndex, 0) - assert.equal(instruction.data.toString('hex'), '771e0eb473e1a7ee03000000030407') - }) - - it('uses caller-provided writable indexes', async () => { - const cct = SolanaTokenManager.fromChain(stubChain()) - const unsigned = await cct.tokenAdminRegistry.generateUnsignedSetPool({ - tokenAddress: KEY, - address: KEY, - poolLookupTableAddress: KEY, - payer: KEY, - writableIndexes: [3, 4, 7, 9], - }) - - assert.equal(unsigned.instructions[0]!.data.toString('hex'), '771e0eb473e1a7ee0400000003040709') - }) - - it('rejects invalid writable indexes before RPC', async () => { - const cct = SolanaTokenManager.fromChain(stubChain()) - await assert.rejects( - () => - cct.tokenAdminRegistry.generateUnsignedSetPool({ - tokenAddress: KEY, - address: KEY, - poolLookupTableAddress: KEY, - payer: KEY, - writableIndexes: [3, 256], - }), - (err: unknown) => - err instanceof CCTParamsInvalidError && - err.context.operation === 'setPool' && - err.context.param === 'writableIndexes[1]', - ) - }) - - it('rejects empty writable indexes before RPC', async () => { - const cct = SolanaTokenManager.fromChain(stubChain()) - await assert.rejects( - () => - cct.tokenAdminRegistry.generateUnsignedSetPool({ - tokenAddress: KEY, - address: KEY, - poolLookupTableAddress: KEY, - payer: KEY, - writableIndexes: [], - }), - (err: unknown) => - err instanceof CCTParamsInvalidError && - err.context.operation === 'setPool' && - err.context.param === 'writableIndexes', - ) - }) - - it('resolves the router from address', async () => { - let requestedAddress: string | undefined - const cct = SolanaTokenManager.fromChain({ - ...stubChain(), - getTokenAdminRegistryFor: async (address: string) => { - requestedAddress = address - return ROUTER - }, - } as SolanaChain) - - const unsigned = await cct.tokenAdminRegistry.generateUnsignedSetPool({ - tokenAddress: KEY, - address: ADDRESS, - poolLookupTableAddress: KEY, - payer: KEY, - }) - - assert.equal(requestedAddress, ADDRESS) - assert.equal(unsigned.instructions[0]!.programId.toBase58(), ROUTER) - }) - - it('validates public keys before RPC', async () => { - const cct = SolanaTokenManager.fromChain(stubChain()) - await assert.rejects( - () => - cct.tokenAdminRegistry.generateUnsignedSetPool({ - tokenAddress: 'nope', - address: KEY, - poolLookupTableAddress: KEY, - payer: KEY, - }), - (err: unknown) => - err instanceof CCTParamsInvalidError && - err.context.operation === 'setPool' && - err.context.param === 'tokenAddress', - ) - }) - - it('rejects a non-wallet before generating setPool', async () => { - const cct = SolanaTokenManager.fromChain(stubChain()) - await assert.rejects( - () => - cct.tokenAdminRegistry.setPool({ - tokenAddress: KEY, - address: KEY, - poolLookupTableAddress: KEY, - wallet: {}, - }), - (err: unknown) => err instanceof CCIPWalletInvalidError, - ) - }) -}) 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 000000000..fb661bdef --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.test.ts @@ -0,0 +1,86 @@ +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 { 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()).tokenAdminRegistry.generateUnsignedSetPool({ + tokenAddress: TOKEN, + address: ADDRESS, + poolLookupTableAddress: POOL_LOOKUP_TABLE, + payer: PAYER, + ...opts, + }) +} + +describe('Solana TokenAdminRegistry setPool', () => { + 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.tokenAdminRegistry.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)) + }) +}) 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 index f7629f215..0f6b8249f 100644 --- 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 @@ -47,7 +47,7 @@ export class SetPool extends SolanaOperation { } /** Builds the unsigned Solana `setPool` instruction set. */ - protected async encode( + protected async buildUnsigned( chain: SolanaChain, opts: GenerateSetPoolParams, ): Promise { From d52a6f71988af998978a07557d0b9151c16967e5 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa Date: Thu, 9 Jul 2026 16:59:19 +0100 Subject: [PATCH 13/87] Rename token admin folder --- .../{token-admin => token-admin-registry}/operations/set-pool.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ccip-sdk/src/cct/evm/{token-admin => token-admin-registry}/operations/set-pool.ts (100%) diff --git a/ccip-sdk/src/cct/evm/token-admin/operations/set-pool.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.ts similarity index 100% rename from ccip-sdk/src/cct/evm/token-admin/operations/set-pool.ts rename to ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.ts From 9f61f9400a7b49476254d6d89405f96b94571c79 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Fri, 10 Jul 2026 18:04:59 +0800 Subject: [PATCH 14/87] fix: export errors, params, returns and flatten tokenmanager --- ccip-sdk/src/cct/solana/index.test.ts | 5 +- ccip-sdk/src/cct/solana/index.ts | 68 +++++++++++++++++-- .../cct/solana/token-admin-registry/index.ts | 28 -------- .../operations/set-pool.test.ts | 4 +- .../operations/set-pool.ts | 20 +++++- 5 files changed, 84 insertions(+), 41 deletions(-) delete mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/index.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 2b78481e6..bf43fc14b 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -14,12 +14,13 @@ function stubChain(): SolanaChain { } describe('SolanaTokenManager (cct/solana)', () => { - it('fromChain exposes grouped TokenAdminRegistry operations', () => { + it('fromChain exposes flat TokenAdminRegistry operations', () => { const chain = stubChain() const cct = SolanaTokenManager.fromChain(chain) assert.equal(cct.chain, chain) assert.equal(cct.provider, chain.connection) - assert.equal(cct.tokenAdminRegistry.chain, chain) + assert.equal(typeof cct.generateUnsignedSetPool, 'function') + assert.equal(typeof cct.setPool, 'function') }) it('creates from a connection provider', async (t) => { diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 966943b6c..f75849ddd 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -12,18 +12,23 @@ import { 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 { SolanaTokenAdminRegistryClient } from './token-admin-registry/index.ts' +import { + type ExecuteSetPoolParams, + type ExecuteSetPoolResult, + type GenerateSetPoolParams, + type GenerateSetPoolResult, + SetPool, +} from './token-admin-registry/operations/set-pool.ts' -/** CCT admin facade for Solana; grouped clients own contract/program operations. */ +/** CCT admin facade for Solana. */ export class SolanaTokenManager extends TokenManager { readonly chain: SolanaChain - readonly tokenAdminRegistry: SolanaTokenAdminRegistryClient + readonly #setPool = new SetPool() /** Creates a Solana CCT manager for an existing chain. */ constructor(chain: SolanaChain) { super() this.chain = chain - this.tokenAdminRegistry = new SolanaTokenAdminRegistryClient(chain) } /** Wraps an existing {@link SolanaChain}. */ @@ -46,7 +51,56 @@ export class SolanaTokenManager extends TokenManager return this.chain.connection } - /** Serializes an unsigned Solana CCT tx for external signing. */ + /** + * Builds unsigned Solana `setPool` instructions. + * + * The `payer` pays transaction fees. `authority` defaults to `payer`; Squads/multisig flows + * should pass the token admin/vault authority explicitly. + * + * @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 wallet must be the token admin authority. + * + * @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) + } + + /** + * Serializes an unsigned Solana CCT tx for external signing. + * + * @example + * ```ts + * 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, @@ -56,5 +110,7 @@ export class SolanaTokenManager extends TokenManager } } -export type { GenerateSetPoolParams, SetPoolParams } from './token-admin-registry/index.ts' +export * from '../errors.ts' +export type { TransactionHash } from '../operation.ts' export type { SerializedSolanaTxEncoding } from './serialize.ts' +export type * from './token-admin-registry/operations/set-pool.ts' diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/index.ts b/ccip-sdk/src/cct/solana/token-admin-registry/index.ts deleted file mode 100644 index 51093e7ae..000000000 --- a/ccip-sdk/src/cct/solana/token-admin-registry/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { type GenerateSetPoolParams, type SetPoolParams, SetPool } from './operations/set-pool.ts' -import type { SolanaChain } from '../../../solana/index.ts' -import type { UnsignedSolanaTx } from '../../../solana/types.ts' -import type { TransactionHash } from '../../operation.ts' -import type { SolanaExecuteParams } from '../operation.ts' - -/** TokenAdminRegistry CCT operations for a Solana Router program. */ -export class SolanaTokenAdminRegistryClient { - readonly chain: SolanaChain - readonly #setPool = new SetPool() - - /** Creates a TokenAdminRegistry client for an existing Solana chain. */ - constructor(chain: SolanaChain) { - this.chain = chain - } - - /** Builds unsigned Solana `setPool` instructions. */ - generateUnsignedSetPool(opts: GenerateSetPoolParams): Promise { - return this.#setPool.generate(this.chain, opts) - } - - /** Registers a token pool. */ - setPool(opts: SolanaExecuteParams): Promise { - return this.#setPool.execute(this.chain, opts) - } -} - -export type { GenerateSetPoolParams, SetPoolParams } from './operations/set-pool.ts' 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 index fb661bdef..df1412f9a 100644 --- 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 @@ -30,7 +30,7 @@ function stubChain(router = ROUTER, onAddress?: (address: string) => void): Sola } function generate(opts = {}) { - return SolanaTokenManager.fromChain(stubChain()).tokenAdminRegistry.generateUnsignedSetPool({ + return SolanaTokenManager.fromChain(stubChain()).generateUnsignedSetPool({ tokenAddress: TOKEN, address: ADDRESS, poolLookupTableAddress: POOL_LOOKUP_TABLE, @@ -67,7 +67,7 @@ describe('Solana TokenAdminRegistry setPool', () => { stubChain(ROUTER, (address) => (requestedAddress = address)), ) - const unsigned = await cct.tokenAdminRegistry.generateUnsignedSetPool({ + const unsigned = await cct.generateUnsignedSetPool({ tokenAddress: TOKEN, address: ADDRESS, poolLookupTableAddress: POOL_LOOKUP_TABLE, 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 index 0f6b8249f..94c6a75b0 100644 --- 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 @@ -5,7 +5,12 @@ 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 SolanaGenerateParams, SolanaOperation } from '../../operation.ts' +import type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' import { createRouterProgram, deriveRouterConfigPda, @@ -16,8 +21,8 @@ import { validatePublicKey, validateWritableIndexes } from '../../validate.ts' /** Standard BurnMint/LockRelease pool ALT writable positions. */ export const DEFAULT_WRITABLE_INDEXES = [3, 4, 7] as const -/** Parameters for Solana TokenAdminRegistry `setPool`. */ -export type SetPoolParams = { +/** Parameters shared by Solana TokenAdminRegistry `setPool` generation and execution. */ +type SetPoolParams = { tokenAddress: string address: string poolLookupTableAddress: string @@ -32,6 +37,15 @@ export type SetPoolParams = { /** Parameters for unsigned Solana TokenAdminRegistry `setPool` generation. */ export type GenerateSetPoolParams = SolanaGenerateParams +/** 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 = TransactionHash + /** Solana TokenAdminRegistry `setPool` operation. */ export class SetPool extends SolanaOperation { readonly name = 'setPool' From 4b284088a871bd894d24c566ff800d0bc939d2c6 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Fri, 10 Jul 2026 18:54:13 +0800 Subject: [PATCH 15/87] fix: address comments --- ccip-sdk/src/cct/solana/index.ts | 18 ++++------ ccip-sdk/src/cct/solana/operation.test.ts | 33 +++++++++++++++++++ ccip-sdk/src/cct/solana/operation.ts | 15 ++++++--- .../operations/create-lookup-table.ts | 21 ++++++++++-- .../token-admin-registry/operations/index.ts | 2 ++ 5 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 45ab4c281..3d953900a 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -15,17 +15,15 @@ import { type SerializedSolanaTxEncoding, serializeUnsignedSolanaTx } from './se import { type ExecuteCreateLookupTableParams, type ExecuteCreateLookupTableResult, - type GenerateCreateLookupTableParams, - type GenerateCreateLookupTableResult, - CreateLookupTable, -} from './token-admin-registry/operations/create-lookup-table.ts' -import { type ExecuteSetPoolParams, type ExecuteSetPoolResult, + type GenerateCreateLookupTableParams, + type GenerateCreateLookupTableResult, type GenerateSetPoolParams, type GenerateSetPoolResult, + CreateLookupTable, SetPool, -} from './token-admin-registry/operations/set-pool.ts' +} from './token-admin-registry/operations/index.ts' /** CCT admin facade for Solana. */ export class SolanaTokenManager extends TokenManager { @@ -76,10 +74,7 @@ export class SolanaTokenManager extends TokenManager generateUnsignedCreateLookupTable( opts: GenerateCreateLookupTableParams, ): Promise { - return this.#createLookupTable.generate( - this.chain, - opts, - ) as Promise + return this.#createLookupTable.generate(this.chain, opts) } /** @@ -161,5 +156,4 @@ export class SolanaTokenManager extends TokenManager export * from '../errors.ts' export type { TransactionHash } from '../operation.ts' export type { SerializedSolanaTxEncoding } from './serialize.ts' -export type * from './token-admin-registry/operations/create-lookup-table.ts' -export type * from './token-admin-registry/operations/set-pool.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 index 499c9273a..c22342da8 100644 --- a/ccip-sdk/src/cct/solana/operation.test.ts +++ b/ccip-sdk/src/cct/solana/operation.test.ts @@ -27,6 +27,27 @@ class TestOperation extends SolanaOperation<{ value: string }> { } } +type TestTx = UnsignedSolanaTx & { lookupTableAddress: string } +type TestResult = { hash: string; lookupTableAddress: string } + +class TestResultOperation extends SolanaOperation<{ value: string }, TestTx, TestResult> { + readonly name = 'testResultOperation' + + protected validate(): void {} + + protected buildUnsigned(): Promise { + return Promise.resolve({ + family: ChainFamily.Solana, + instructions: [], + lookupTableAddress: 'lookup-table', + }) + } + + protected override resultFromGenerated(hash: { hash: string }, tx: TestTx): TestResult { + return { ...hash, lookupTableAddress: tx.lookupTableAddress } + } +} + const chain = { logger: console, connection: {} } as unknown as SolanaChain describe('SolanaOperation', () => { @@ -57,6 +78,18 @@ describe('SolanaOperation', () => { assert.equal(op.captured, wallet.publicKey.toBase58()) }) + it('lets operations add generated data to execute results', async () => { + const op = new TestResultOperation() + const wallet = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, + } + + const result = await op.execute(chain, { value: 'x', wallet }) + + assert.equal(result.lookupTableAddress, 'lookup-table') + }) + it('rejects invalid wallets before validation or building unsigned txs', async () => { const op = new TestOperation() diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts index ed6b13a9f..28676dc6b 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -32,10 +32,16 @@ function withPayer

( export abstract class SolanaOperation< P extends object, Tx extends UnsignedSolanaTx = UnsignedSolanaTx, -> extends Operation, Tx> { + Result = TransactionHash, +> extends Operation, Tx, Result> { /** Build instructions after params have been validated. */ protected abstract buildUnsigned(chain: SolanaChain, params: SolanaGenerateParams

): Promise + /** Adds generated operation metadata to the submit result. */ + protected resultFromGenerated(hash: TransactionHash, _tx: Tx): Result { + return hash as Result + } + /** Run {@link validate} and {@link buildUnsigned}; no signing. */ async generate(chain: SolanaChain, params: SolanaGenerateParams

): Promise { this.validate(params) @@ -43,11 +49,12 @@ export abstract class SolanaOperation< } /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ - async execute(chain: SolanaChain, params: SolanaExecuteParams

): Promise { + async execute(chain: SolanaChain, params: SolanaExecuteParams

): Promise { const { wallet, computeUnits } = params if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) - const unsigned = await this.generate(chain, withPayer(params, wallet.publicKey.toBase58())) - return submit(chain, wallet, unsigned, this.name, computeUnits) + const tx = await this.generate(chain, withPayer(params, wallet.publicKey.toBase58())) + const hash = await submit(chain, wallet, tx, this.name, computeUnits) + return this.resultFromGenerated(hash, tx) } } 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 index 3cdc5daf3..fad17a027 100644 --- 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 @@ -43,10 +43,17 @@ export type GenerateCreateLookupTableResult = UnsignedSolanaTx & { export type ExecuteCreateLookupTableParams = SolanaExecuteParams /** Result of executing Solana TokenAdminRegistry `createLookupTable`. */ -export type ExecuteCreateLookupTableResult = TransactionHash +export type CreateLookupTableResult = TransactionHash & { lookupTableAddress: string } + +/** Result alias for executing Solana TokenAdminRegistry `createLookupTable`. */ +export type ExecuteCreateLookupTableResult = CreateLookupTableResult /** Builds and submits Solana ALT create+extend instructions for token pool setup. */ -export class CreateLookupTable extends SolanaOperation { +export class CreateLookupTable extends SolanaOperation< + CreateLookupTableParams, + GenerateCreateLookupTableResult, + CreateLookupTableResult +> { readonly name = 'createLookupTable' /** Validates all public keys before any RPC. */ @@ -74,7 +81,7 @@ export class CreateLookupTable extends SolanaOperation const [createIx, lookupTableAddress] = AddressLookupTableProgram.createLookupTable({ authority, payer, - recentSlot: await chain.connection.getSlot(), + recentSlot: await chain.connection.getSlot('finalized'), }) const { tokenProgram } = await resolveATA(chain.connection, tokenMint, authority) @@ -133,4 +140,12 @@ export class CreateLookupTable extends SolanaOperation lookupTableAddress: lookupTableAddress.toBase58(), } } + + /** Adds the generated lookup table address to the execute result. */ + protected override resultFromGenerated( + hash: TransactionHash, + tx: GenerateCreateLookupTableResult, + ): CreateLookupTableResult { + return { ...hash, lookupTableAddress: tx.lookupTableAddress } + } } 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 000000000..710924c1b --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -0,0 +1,2 @@ +export * from './create-lookup-table.ts' +export * from './set-pool.ts' From 17e748d27ba2a3a83fb533aeac56d76871f3224e Mon Sep 17 00:00:00 2001 From: mervin-link Date: Fri, 10 Jul 2026 19:01:19 +0800 Subject: [PATCH 16/87] fix: duplicate result type --- .../operations/create-lookup-table.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 index fad17a027..65a0ad9f9 100644 --- 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 @@ -43,16 +43,13 @@ export type GenerateCreateLookupTableResult = UnsignedSolanaTx & { export type ExecuteCreateLookupTableParams = SolanaExecuteParams /** Result of executing Solana TokenAdminRegistry `createLookupTable`. */ -export type CreateLookupTableResult = TransactionHash & { lookupTableAddress: string } - -/** Result alias for executing Solana TokenAdminRegistry `createLookupTable`. */ -export type ExecuteCreateLookupTableResult = CreateLookupTableResult +export type ExecuteCreateLookupTableResult = TransactionHash & { lookupTableAddress: string } /** Builds and submits Solana ALT create+extend instructions for token pool setup. */ export class CreateLookupTable extends SolanaOperation< CreateLookupTableParams, GenerateCreateLookupTableResult, - CreateLookupTableResult + ExecuteCreateLookupTableResult > { readonly name = 'createLookupTable' @@ -145,7 +142,7 @@ export class CreateLookupTable extends SolanaOperation< protected override resultFromGenerated( hash: TransactionHash, tx: GenerateCreateLookupTableResult, - ): CreateLookupTableResult { + ): ExecuteCreateLookupTableResult { return { ...hash, lookupTableAddress: tx.lookupTableAddress } } } From e6cfa015a8e0e569dbbdfafc36cd2d008ef57b42 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Fri, 10 Jul 2026 21:06:00 +0800 Subject: [PATCH 17/87] feat: add two ALT mode: create+extend or createEmpty --- ccip-sdk/src/cct/solana/index.ts | 23 ++++---- .../operations/create-lookup-table.test.ts | 31 +++++++++++ .../operations/create-lookup-table.ts | 53 ++++++++++++++----- 3 files changed, 85 insertions(+), 22 deletions(-) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 3d953900a..056e3c903 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -58,16 +58,19 @@ export class SolanaTokenManager extends TokenManager } /** - * Builds unsigned Solana pool lookup table create+extend instructions. + * Builds unsigned Solana pool lookup table instructions. + * + * Defaults to create+extend. 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`. * * @example * ```ts * const cct = SolanaTokenManager.fromChain(chain) * const unsigned = await cct.generateUnsignedCreateLookupTable({ - * tokenAddress: mint, - * poolProgramAddress: poolProgram, - * payer: squadsVault, - * authority: tokenAdmin, + * mode: 'createEmpty', + * payer: eoa, + * authority: squadsVault, * }) * ``` */ @@ -78,14 +81,16 @@ export class SolanaTokenManager extends TokenManager } /** - * Creates and extends a Solana pool lookup table. + * 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. * * @example * ```ts * const cct = SolanaTokenManager.fromChain(chain) - * const { hash } = await cct.createLookupTable({ - * tokenAddress: mint, - * poolProgramAddress: poolProgram, + * const { hash, lookupTableAddress } = await cct.createLookupTable({ + * mode: 'createEmpty', + * authority: squadsVault, * wallet, * }) * ``` 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 index 7ec1a0879..530ce7e16 100644 --- 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 @@ -59,6 +59,37 @@ describe('Solana TokenAdminRegistry createLookupTable', () => { ) }) + 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.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === AUTHORITY)) + }) + + 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(), 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 index 65a0ad9f9..6d323eb6f 100644 --- 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 @@ -23,13 +23,25 @@ import { validatePublicKey } 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 = { - tokenAddress: string - poolProgramAddress: string - additionalAddresses?: string[] - authority?: string -} +type CreateLookupTableParams = + | { + /** Defaults to `createAndExtend`; use `createEmpty` to skip extending the ALT. */ + mode?: Extract + tokenAddress: string + poolProgramAddress: 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 @@ -45,7 +57,7 @@ export type ExecuteCreateLookupTableParams = SolanaExecuteParams { - const poolProgram = new PublicKey(opts.poolProgramAddress) - const tokenMint = new PublicKey(opts.tokenAddress) const payer = new PublicKey(opts.payer) const authority = new PublicKey(opts.authority ?? opts.payer) - const additionalAddresses = (opts.additionalAddresses ?? []).map((a) => new PublicKey(a)) const [createIx, lookupTableAddress] = AddressLookupTableProgram.createLookupTable({ authority, @@ -81,6 +92,22 @@ export class CreateLookupTable extends SolanaOperation< recentSlot: await chain.connection.getSlot('finalized'), }) + if (opts.mode === 'createEmpty') { + chain.logger.debug( + `${this.name}: mode = createEmpty, lookupTable = ${lookupTableAddress.toBase58()}`, + ) + return { + family: ChainFamily.Solana, + instructions: [createIx], + mainIndex: 0, + lookupTableAddress: lookupTableAddress.toBase58(), + } + } + + const poolProgram = new PublicKey(opts.poolProgramAddress) + const tokenMint = new PublicKey(opts.tokenAddress) + const additionalAddresses = (opts.additionalAddresses ?? []).map((a) => new PublicKey(a)) + const { tokenProgram } = await resolveATA(chain.connection, tokenMint, authority) const poolConfig = deriveTokenPoolConfigPda(poolProgram, tokenMint) const { router: routerAddress } = await chain.getTokenPoolConfig(poolConfig.toBase58()) From dae8d03da38cd5509a7b18e3de63ec5ed4e2470f Mon Sep 17 00:00:00 2001 From: mervin-link Date: Fri, 10 Jul 2026 21:16:22 +0800 Subject: [PATCH 18/87] fix: add checks to make sure authority === payer when mode is create+extend --- .../operations/create-lookup-table.test.ts | 20 +++++++++++++ .../operations/create-lookup-table.ts | 30 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) 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 index 530ce7e16..ba9f976a4 100644 --- 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 @@ -15,6 +15,10 @@ 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(): SolanaChain { return { @@ -99,6 +103,22 @@ describe('Solana TokenAdminRegistry createLookupTable', () => { assert.equal(unsigned.instructions.length, 3) }) + 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', + ) + }) + it('uses caller-provided authority', async () => { const unsigned = await generate({ authority: 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 index 6d323eb6f..dbb8e7567 100644 --- 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 @@ -1,9 +1,10 @@ import { getAssociatedTokenAddressSync } from '@solana/spl-token' import { AddressLookupTableProgram, 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 } from '../../../../solana/types.ts' +import { type UnsignedSolanaTx, isWallet } from '../../../../solana/types.ts' import { resolveATA } from '../../../../solana/utils.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionHash } from '../../../operation.ts' @@ -18,6 +19,7 @@ import { deriveTokenAdminRegistryPda, } from '../../programs/router.ts' import { deriveTokenPoolConfigPda, deriveTokenPoolSignerPda } from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' import { validatePublicKey } from '../../validate.ts' const MAX_ALT_ADDRESSES = 256 @@ -172,4 +174,30 @@ export class CreateLookupTable extends SolanaOperation< ): ExecuteCreateLookupTableResult { return { ...hash, lookupTableAddress: tx.lookupTableAddress } } + + /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ + override async execute( + chain: SolanaChain, + params: ExecuteCreateLookupTableParams, + ): Promise { + const { wallet, computeUnits, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const payer = wallet.publicKey.toBase58() + if ( + params.mode !== 'createEmpty' && + params.authority && + !new PublicKey(params.authority).equals(wallet.publicKey) + ) { + throw new CCTParamsInvalidError( + this.name, + 'authority', + "createAndExtend requires authority to be the executing wallet. Use mode: 'createEmpty' for vault-owned ALTs.", + ) + } + + const tx = await this.generate(chain, { ...rest, payer }) + const hash = await submit(chain, wallet, tx, this.name, computeUnits) + return this.resultFromGenerated(hash, tx) + } } From 54819588d64b32090ad09dbccffafc44ae8c5a16 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Fri, 10 Jul 2026 23:31:09 +0800 Subject: [PATCH 19/87] feat: add append to lookup table op --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 54 ++++++ ccip-sdk/src/cct/solana/programs/alt.ts | 46 +++++ .../operations/append-to-lookup-table.test.ts | 142 +++++++++++++++ .../operations/append-to-lookup-table.ts | 171 ++++++++++++++++++ .../operations/create-lookup-table.ts | 39 +--- .../token-admin-registry/operations/index.ts | 1 + 7 files changed, 423 insertions(+), 32 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/programs/alt.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index f495a128e..5a56eb706 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -21,6 +21,8 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(cct.provider, chain.connection) 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.generateUnsignedSetPool, 'function') assert.equal(typeof cct.setPool, 'function') }) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 056e3c903..47e25703d 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -13,14 +13,19 @@ import type { UnsignedSolanaTx } from '../../solana/types.ts' import { TokenManager } from '../token-manager.ts' import { type SerializedSolanaTxEncoding, serializeUnsignedSolanaTx } from './serialize.ts' import { + type ExecuteAppendToLookupTableParams, + type ExecuteAppendToLookupTableResult, type ExecuteCreateLookupTableParams, type ExecuteCreateLookupTableResult, type ExecuteSetPoolParams, type ExecuteSetPoolResult, + type GenerateAppendToLookupTableParams, + type GenerateAppendToLookupTableResult, type GenerateCreateLookupTableParams, type GenerateCreateLookupTableResult, type GenerateSetPoolParams, type GenerateSetPoolResult, + AppendToLookupTable, CreateLookupTable, SetPool, } from './token-admin-registry/operations/index.ts' @@ -28,6 +33,7 @@ import { /** CCT admin facade for Solana. */ export class SolanaTokenManager extends TokenManager { readonly chain: SolanaChain + readonly #appendToLookupTable = new AppendToLookupTable() readonly #createLookupTable = new CreateLookupTable() readonly #setPool = new SetPool() @@ -99,6 +105,54 @@ export class SolanaTokenManager extends TokenManager return this.#createLookupTable.execute(this.chain, opts) } + /** + * Builds unsigned Solana lookup table extend instructions. + * + * Pass `tokenAddress` and `poolProgramAddress` to append the standard CCIP pool addresses; + * pass `additionalAddresses` to append manual addresses. `authority` defaults to `payer`. + * + * @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` and `poolProgramAddress` to append the standard CCIP pool addresses; + * + * @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 unsigned Solana `setPool` instructions. * 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 000000000..59ff4f5f3 --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/alt.ts @@ -0,0 +1,46 @@ +import { getAssociatedTokenAddressSync } from '@solana/spl-token' +import { PublicKey } 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 { resolveATA } from '../../../solana/utils.ts' + +type DeriveCcipLookupTableAddressesParams = { + lookupTableAddress: PublicKey + tokenMint: PublicKey + poolProgram: PublicKey + authority: PublicKey +} + +/** Derives the standard CCIP token pool addresses stored in a pool lookup table. */ +export async function deriveCcipLookupTableAddresses( + chain: SolanaChain, + { lookupTableAddress, tokenMint, poolProgram, authority }: DeriveCcipLookupTableAddressesParams, +): Promise { + const { tokenProgram } = await resolveATA(chain.connection, tokenMint, authority) + 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/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 000000000..30680875b --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.test.ts @@ -0,0 +1,142 @@ +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' + +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 WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function stubChain(addresses: PublicKey[] = [], authority = AUTHORITY): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }), + getAddressLookupTable: async () => ({ + 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('Solana TokenAdminRegistry appendToLookupTable', () => { + 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 unsigned = await generate({ tokenAddress: TOKEN, poolProgramAddress: POOL_PROGRAM }) + + assert.equal(unsigned.instructions.length, 1) + }) + + 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', + ) + }) + + it('rejects authority mismatch', async () => { + await assert.rejects( + () => generate({}, stubChain([], 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(currentAddresses)), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'additionalAddresses', + ) + }) + + 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, poolProgramAddress: undefined }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'appendToLookupTable' && + err.context.param === 'tokenAddress', + ) + }) +}) 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 000000000..ee0929285 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.ts @@ -0,0 +1,171 @@ +import { AddressLookupTableProgram, 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 { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { deriveCcipLookupTableAddresses } from '../../programs/alt.ts' +import { submit } from '../../submit.ts' +import { validatePublicKey } from '../../validate.ts' + +const MAX_ALT_ADDRESSES = 256 +const EXTEND_CHUNK_SIZE = 30 + +/** Parameters shared by Solana TokenAdminRegistry `appendToLookupTable` generation and execution. */ +type AppendToLookupTableParams = { + lookupTableAddress: string + tokenAddress?: string + poolProgramAddress?: string + additionalAddresses?: string[] + /** ALT authority. Defaults to payer for unsigned generation and wallet public key for execute. */ + authority?: string +} + +/** Parameters for unsigned Solana lookup table append generation. */ +export type GenerateAppendToLookupTableParams = SolanaGenerateParams + +/** 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 = TransactionHash + +/** Builds and submits Solana ALT extend instructions for token pool setup. */ +export class AppendToLookupTable extends SolanaOperation< + AppendToLookupTableParams, + GenerateAppendToLookupTableResult, + ExecuteAppendToLookupTableResult +> { + readonly name = 'appendToLookupTable' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateAppendToLookupTableParams): void { + validatePublicKey(this.name, 'lookupTableAddress', params.lookupTableAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + if (params.tokenAddress) validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + if (params.poolProgramAddress) { + validatePublicKey(this.name, 'poolProgramAddress', params.poolProgramAddress) + } + for (const [i, address] of (params.additionalAddresses ?? []).entries()) { + validatePublicKey(this.name, `additionalAddresses[${i}]`, address) + } + + if (Boolean(params.tokenAddress) !== Boolean(params.poolProgramAddress)) { + throw new CCTParamsInvalidError( + this.name, + 'tokenAddress', + 'tokenAddress and poolProgramAddress must be provided together', + ) + } + if (!params.tokenAddress && !params.additionalAddresses?.length) { + throw new CCTParamsInvalidError( + this.name, + 'additionalAddresses', + 'must provide tokenAddress/poolProgramAddress or additionalAddresses', + ) + } + } + + /** Builds unsigned ALT extend instructions. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateAppendToLookupTableParams, + ): Promise { + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + const lookupTableAddress = new PublicKey(opts.lookupTableAddress) + 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 ?? []).map((a) => new PublicKey(a))] + + if (opts.tokenAddress && opts.poolProgramAddress) { + const poolProgram = new PublicKey(opts.poolProgramAddress) + const tokenMint = new PublicKey(opts.tokenAddress) + const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { + lookupTableAddress, + tokenMint, + poolProgram, + authority, + }) + 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 = [] + 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, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const payer = wallet.publicKey.toBase58() + if (params.authority && !new PublicKey(params.authority).equals(wallet.publicKey)) { + throw new CCTParamsInvalidError( + this.name, + 'authority', + 'appendToLookupTable requires authority to be the executing wallet.', + ) + } + + const tx = await this.generate(chain, { ...rest, payer }) + return submit(chain, wallet, tx, this.name, computeUnits) + } +} 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 index dbb8e7567..81559a65e 100644 --- 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 @@ -1,11 +1,9 @@ -import { getAssociatedTokenAddressSync } from '@solana/spl-token' import { AddressLookupTableProgram, 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 { resolveATA } from '../../../../solana/utils.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionHash } from '../../../operation.ts' import { @@ -13,12 +11,7 @@ import { type SolanaGenerateParams, SolanaOperation, } from '../../operation.ts' -import { deriveFeeBillingTokenConfigPda } from '../../programs/fee-quoter.ts' -import { - deriveExternalTokenPoolsSignerPda, - deriveTokenAdminRegistryPda, -} from '../../programs/router.ts' -import { deriveTokenPoolConfigPda, deriveTokenPoolSignerPda } from '../../programs/token-pool.ts' +import { deriveCcipLookupTableAddresses } from '../../programs/alt.ts' import { submit } from '../../submit.ts' import { validatePublicKey } from '../../validate.ts' @@ -110,31 +103,13 @@ export class CreateLookupTable extends SolanaOperation< const tokenMint = new PublicKey(opts.tokenAddress) const additionalAddresses = (opts.additionalAddresses ?? []).map((a) => new PublicKey(a)) - const { tokenProgram } = await resolveATA(chain.connection, tokenMint, authority) - 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) - - const addresses = [ + const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { lookupTableAddress, - tokenAdminRegistry, - poolProgram, - poolConfig, - poolTokenAta, - poolSigner, - tokenProgram, tokenMint, - feeTokenConfig, - routerPoolSigner, - ...additionalAddresses, - ] + poolProgram, + authority, + }) + const addresses = [...ccipAddresses, ...additionalAddresses] if (addresses.length > MAX_ALT_ADDRESSES) { throw new CCTParamsInvalidError( @@ -157,7 +132,7 @@ export class CreateLookupTable extends SolanaOperation< } chain.logger.debug( - `${this.name}: router = ${router.toBase58()}, token = ${tokenMint.toBase58()}, lookupTable = ${lookupTableAddress.toBase58()}`, + `${this.name}: token = ${tokenMint.toBase58()}, lookupTable = ${lookupTableAddress.toBase58()}`, ) return { family: ChainFamily.Solana, 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 index 710924c1b..a054baa3f 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -1,2 +1,3 @@ +export * from './append-to-lookup-table.ts' export * from './create-lookup-table.ts' export * from './set-pool.ts' From a78cf3fa0b1fbd7475006ad7be31c3709851f541 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Mon, 13 Jul 2026 11:24:16 +0800 Subject: [PATCH 20/87] fix: add own create alt instruction --- ccip-sdk/src/cct/solana/programs/alt.ts | 57 +++++++++++++++++++ .../operations/create-lookup-table.test.ts | 9 ++- .../operations/create-lookup-table.ts | 3 +- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/programs/alt.ts 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 000000000..73bd58e96 --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/alt.ts @@ -0,0 +1,57 @@ +import { Buffer } from 'buffer' + +import { + AddressLookupTableProgram, + PublicKey, + SystemProgram, + TransactionInstruction, +} from '@solana/web3.js' + +const CREATE_LOOKUP_TABLE_DISCRIMINATOR = 0 +const CREATE_LOOKUP_TABLE_DATA_LENGTH = 13 + +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, + }), + } +} 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 index ba9f976a4..50f92f5b7 100644 --- 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 @@ -61,6 +61,10 @@ describe('Solana TokenAdminRegistry createLookupTable', () => { unsigned.instructions[1]!.programId.toBase58(), AddressLookupTableProgram.programId.toBase58(), ) + assert.equal( + unsigned.instructions[0]!.keys.find((key) => key.pubkey.toBase58() === PAYER)?.isSigner, + false, + ) }) it('builds create-only ALT instruction in createEmpty mode', async () => { @@ -80,7 +84,10 @@ describe('Solana TokenAdminRegistry createLookupTable', () => { unsigned.instructions[0]!.programId.toBase58(), AddressLookupTableProgram.programId.toBase58(), ) - assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === AUTHORITY)) + assert.equal( + unsigned.instructions[0]!.keys.find((key) => key.pubkey.toBase58() === AUTHORITY)?.isSigner, + false, + ) }) it('defaults createEmpty authority to payer', async () => { 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 index dbb8e7567..70f34dce5 100644 --- 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 @@ -13,6 +13,7 @@ import { type SolanaGenerateParams, SolanaOperation, } from '../../operation.ts' +import { buildCreateLookupTableInstruction } from '../../programs/alt.ts' import { deriveFeeBillingTokenConfigPda } from '../../programs/fee-quoter.ts' import { deriveExternalTokenPoolsSignerPda, @@ -88,7 +89,7 @@ export class CreateLookupTable extends SolanaOperation< const payer = new PublicKey(opts.payer) const authority = new PublicKey(opts.authority ?? opts.payer) - const [createIx, lookupTableAddress] = AddressLookupTableProgram.createLookupTable({ + const { instruction: createIx, lookupTableAddress } = buildCreateLookupTableInstruction({ authority, payer, recentSlot: await chain.connection.getSlot('finalized'), From 1c0f2a1a6e9588afbffc7c18aa3a7d8938206c9c Mon Sep 17 00:00:00 2001 From: Pedro Barbosa Date: Mon, 13 Jul 2026 11:21:32 +0100 Subject: [PATCH 21/87] Add @examples --- ccip-sdk/src/cct/errors.ts | 37 ++++++++++++++++++++++++++++++++++- ccip-sdk/src/cct/evm/index.ts | 24 +++++++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/ccip-sdk/src/cct/errors.ts b/ccip-sdk/src/cct/errors.ts index 1c2f32a49..f36c79a77 100644 --- a/ccip-sdk/src/cct/errors.ts +++ b/ccip-sdk/src/cct/errors.ts @@ -9,7 +9,20 @@ import { type CCIPErrorOptions, CCIPError, CCIPErrorCode } from '../errors/index // Parameter validation -/** Thrown before any RPC when operation params fail validation. Permanent. */ +/** + * 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. */ @@ -32,6 +45,17 @@ export class CCTParamsInvalidError extends CCIPError { * Thrown when a CCT write fails before broadcast or the transaction reverts after mining. * Pre-broadcast failures (signing/RPC) may set `isTransient: true` for network errors; * on-chain reverts are permanent. Reverts 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' @@ -48,6 +72,17 @@ export class CCTTxFailedError extends CCIPError { /** * 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' diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 73ab0e82c..c08b69f6e 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -14,7 +14,7 @@ import type { UnsignedEVMTx } from '../../evm/types.ts' import type { ChainFamily } from '../../networks.ts' import type { TransactionHash } from '../operation.ts' import { TokenManager } from '../token-manager.ts' -import { type SetPoolParams, SetPool } from './token-admin/operations/set-pool.ts' +import { type SetPoolParams, SetPool } from './token-admin-registry/operations/set-pool.ts' /** CCT admin operations for EVM chains, delegating each op to an operation class. */ export class EVMTokenManager extends TokenManager { @@ -54,6 +54,16 @@ export class EVMTokenManager extends TokenManager { * 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) @@ -65,6 +75,16 @@ export class EVMTokenManager extends TokenManager { * @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: SetPoolParams & { wallet: unknown }): Promise { return this.#setPool.execute(this.chain, opts) @@ -72,5 +92,5 @@ export class EVMTokenManager extends TokenManager { } export * from '../errors.ts' -export type { SetPoolParams } from './token-admin/operations/set-pool.ts' +export type { SetPoolParams } from './token-admin-registry/operations/set-pool.ts' export type { TransactionHash } from '../operation.ts' From 6b3070db704b49b29c427a97bcbaad199206eb75 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa Date: Mon, 13 Jul 2026 15:02:50 +0100 Subject: [PATCH 22/87] Ran generate and lint --- ccip-cli/src/index.ts | 2 +- ccip-sdk/src/api/index.ts | 2 +- ccip-sdk/src/selectors.ts | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 7cfb30c30..20bb80e93 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -28,7 +28,7 @@ Error.stackTraceLimit = 50 // show more stack frames for better debugging // generate:nofail // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -const VERSION = '1.10.2-73e631c' +const VERSION = '1.10.2-659a810' // generate:end const require = createRequire(import.meta.url) diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index fdd4cbed7..8674ca66b 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -62,7 +62,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.10.2-73e631c' +export const SDK_VERSION = '1.10.2-659a810' // generate:end /** SDK telemetry header name */ diff --git a/ccip-sdk/src/selectors.ts b/ccip-sdk/src/selectors.ts index 812f25e91..277f3c877 100644 --- a/ccip-sdk/src/selectors.ts +++ b/ccip-sdk/src/selectors.ts @@ -1420,6 +1420,12 @@ const SELECTORS: Selectors = { network_type: 'TESTNET', family: 'EVM', }, + '364301': { + selector: 17611928792452358269n, + name: 't-rex-testnet', + network_type: 'TESTNET', + family: 'EVM', + }, '421613': { selector: 6101244977088475029n, name: 'ethereum-testnet-goerli-arbitrum-1', From 517a1d9d74e5bc7a916da2b6508e365c1c95ebfd Mon Sep 17 00:00:00 2001 From: mervin-link Date: Mon, 13 Jul 2026 23:46:12 +0800 Subject: [PATCH 23/87] fix: import buffer and ix type --- ccip-sdk/src/cct/solana/programs/fee-quoter.ts | 2 ++ ccip-sdk/src/cct/solana/programs/token-pool.ts | 2 ++ .../token-admin-registry/operations/create-lookup-table.ts | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ccip-sdk/src/cct/solana/programs/fee-quoter.ts b/ccip-sdk/src/cct/solana/programs/fee-quoter.ts index 4d35ed9b6..71b1dd8a0 100644 --- a/ccip-sdk/src/cct/solana/programs/fee-quoter.ts +++ b/ccip-sdk/src/cct/solana/programs/fee-quoter.ts @@ -1,3 +1,5 @@ +import { Buffer } from 'buffer' + import { PublicKey } from '@solana/web3.js' /** Derives the FeeQuoter billing token config PDA for a mint. */ diff --git a/ccip-sdk/src/cct/solana/programs/token-pool.ts b/ccip-sdk/src/cct/solana/programs/token-pool.ts index 7901f78e1..8ae6e1064 100644 --- a/ccip-sdk/src/cct/solana/programs/token-pool.ts +++ b/ccip-sdk/src/cct/solana/programs/token-pool.ts @@ -1,3 +1,5 @@ +import { Buffer } from 'buffer' + import { PublicKey } from '@solana/web3.js' /** Derives a token pool state/config PDA for a mint. */ 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 index 70f34dce5..ac8f0ddf7 100644 --- 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 @@ -1,5 +1,5 @@ import { getAssociatedTokenAddressSync } from '@solana/spl-token' -import { AddressLookupTableProgram, PublicKey } from '@solana/web3.js' +import { type TransactionInstruction, AddressLookupTableProgram, PublicKey } from '@solana/web3.js' import { CCIPWalletInvalidError } from '../../../../errors/index.ts' import { ChainFamily } from '../../../../networks.ts' @@ -145,7 +145,7 @@ export class CreateLookupTable extends SolanaOperation< ) } - const extendIxs = [] + const extendIxs: TransactionInstruction[] = [] for (let i = 0; i < addresses.length; i += EXTEND_CHUNK_SIZE) { extendIxs.push( AddressLookupTableProgram.extendLookupTable({ From 0511cc6f1968fc325e362688bfbef2d6fb4cf397 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Tue, 14 Jul 2026 00:10:44 +0800 Subject: [PATCH 24/87] fix: add tsdoc --- .../token-admin-registry/operations/set-pool.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 index 94c6a75b0..af35b25f3 100644 --- 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 @@ -24,8 +24,19 @@ 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. From 9ace78ef031657feae87251efec0367356e5b660 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Tue, 14 Jul 2026 22:40:01 +0800 Subject: [PATCH 25/87] fix: remove helper function and override execute --- ccip-sdk/src/cct/solana/operation.test.ts | 33 ------------------- ccip-sdk/src/cct/solana/operation.ts | 13 ++------ .../operations/create-lookup-table.ts | 13 ++------ 3 files changed, 5 insertions(+), 54 deletions(-) diff --git a/ccip-sdk/src/cct/solana/operation.test.ts b/ccip-sdk/src/cct/solana/operation.test.ts index c22342da8..499c9273a 100644 --- a/ccip-sdk/src/cct/solana/operation.test.ts +++ b/ccip-sdk/src/cct/solana/operation.test.ts @@ -27,27 +27,6 @@ class TestOperation extends SolanaOperation<{ value: string }> { } } -type TestTx = UnsignedSolanaTx & { lookupTableAddress: string } -type TestResult = { hash: string; lookupTableAddress: string } - -class TestResultOperation extends SolanaOperation<{ value: string }, TestTx, TestResult> { - readonly name = 'testResultOperation' - - protected validate(): void {} - - protected buildUnsigned(): Promise { - return Promise.resolve({ - family: ChainFamily.Solana, - instructions: [], - lookupTableAddress: 'lookup-table', - }) - } - - protected override resultFromGenerated(hash: { hash: string }, tx: TestTx): TestResult { - return { ...hash, lookupTableAddress: tx.lookupTableAddress } - } -} - const chain = { logger: console, connection: {} } as unknown as SolanaChain describe('SolanaOperation', () => { @@ -78,18 +57,6 @@ describe('SolanaOperation', () => { assert.equal(op.captured, wallet.publicKey.toBase58()) }) - it('lets operations add generated data to execute results', async () => { - const op = new TestResultOperation() - const wallet = { - publicKey: Keypair.generate().publicKey, - signTransaction: async (tx: T) => tx, - } - - const result = await op.execute(chain, { value: 'x', wallet }) - - assert.equal(result.lookupTableAddress, 'lookup-table') - }) - it('rejects invalid wallets before validation or building unsigned txs', async () => { const op = new TestOperation() diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts index 28676dc6b..5b870b067 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -32,16 +32,10 @@ function withPayer

( export abstract class SolanaOperation< P extends object, Tx extends UnsignedSolanaTx = UnsignedSolanaTx, - Result = TransactionHash, -> extends Operation, Tx, Result> { +> extends Operation, Tx, TransactionHash> { /** Build instructions after params have been validated. */ protected abstract buildUnsigned(chain: SolanaChain, params: SolanaGenerateParams

): Promise - /** Adds generated operation metadata to the submit result. */ - protected resultFromGenerated(hash: TransactionHash, _tx: Tx): Result { - return hash as Result - } - /** Run {@link validate} and {@link buildUnsigned}; no signing. */ async generate(chain: SolanaChain, params: SolanaGenerateParams

): Promise { this.validate(params) @@ -49,12 +43,11 @@ export abstract class SolanaOperation< } /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ - async execute(chain: SolanaChain, params: SolanaExecuteParams

): Promise { + async execute(chain: SolanaChain, params: SolanaExecuteParams

): Promise { const { wallet, computeUnits } = params if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) const tx = await this.generate(chain, withPayer(params, wallet.publicKey.toBase58())) - const hash = await submit(chain, wallet, tx, this.name, computeUnits) - return this.resultFromGenerated(hash, tx) + return submit(chain, wallet, tx, this.name, computeUnits) } } 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 index ac8f0ddf7..884c9448c 100644 --- 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 @@ -63,8 +63,7 @@ export type ExecuteCreateLookupTableResult = TransactionHash & { lookupTableAddr /** Builds and submits Solana ALT create instructions, optionally with extend instructions. */ export class CreateLookupTable extends SolanaOperation< CreateLookupTableParams, - GenerateCreateLookupTableResult, - ExecuteCreateLookupTableResult + GenerateCreateLookupTableResult > { readonly name = 'createLookupTable' @@ -168,14 +167,6 @@ export class CreateLookupTable extends SolanaOperation< } } - /** Adds the generated lookup table address to the execute result. */ - protected override resultFromGenerated( - hash: TransactionHash, - tx: GenerateCreateLookupTableResult, - ): ExecuteCreateLookupTableResult { - return { ...hash, lookupTableAddress: tx.lookupTableAddress } - } - /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ override async execute( chain: SolanaChain, @@ -199,6 +190,6 @@ export class CreateLookupTable extends SolanaOperation< const tx = await this.generate(chain, { ...rest, payer }) const hash = await submit(chain, wallet, tx, this.name, computeUnits) - return this.resultFromGenerated(hash, tx) + return { ...hash, lookupTableAddress: tx.lookupTableAddress } } } From 4d086cb564f83f398535fe72bd277430bca4c66d Mon Sep 17 00:00:00 2001 From: mervin-link Date: Tue, 14 Jul 2026 23:09:28 +0800 Subject: [PATCH 26/87] fix: SolanaOperation type changes --- .../token-admin-registry/operations/append-to-lookup-table.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 075251bed..9004ba90a 100644 --- 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 @@ -43,8 +43,7 @@ export type ExecuteAppendToLookupTableResult = TransactionHash /** Builds and submits Solana ALT extend instructions for token pool setup. */ export class AppendToLookupTable extends SolanaOperation< AppendToLookupTableParams, - GenerateAppendToLookupTableResult, - ExecuteAppendToLookupTableResult + GenerateAppendToLookupTableResult > { readonly name = 'appendToLookupTable' From fc2198299ad16dc840ea85824cb95c542a2d9a2a Mon Sep 17 00:00:00 2001 From: mervin-link Date: Wed, 15 Jul 2026 19:32:37 +0800 Subject: [PATCH 27/87] fix: validate authority as public key first --- .../operations/create-lookup-table.ts | 9 +++------ ccip-sdk/src/cct/solana/validate.ts | 11 ++++++++--- 2 files changed, 11 insertions(+), 9 deletions(-) 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 index 884c9448c..33e1c26ba 100644 --- 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 @@ -21,7 +21,7 @@ import { } from '../../programs/router.ts' import { deriveTokenPoolConfigPda, deriveTokenPoolSignerPda } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' -import { validatePublicKey } from '../../validate.ts' +import { parsePublicKey, validatePublicKey } from '../../validate.ts' const MAX_ALT_ADDRESSES = 256 const EXTEND_CHUNK_SIZE = 30 @@ -176,11 +176,8 @@ export class CreateLookupTable extends SolanaOperation< if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) const payer = wallet.publicKey.toBase58() - if ( - params.mode !== 'createEmpty' && - params.authority && - !new PublicKey(params.authority).equals(wallet.publicKey) - ) { + const authority = params.authority && parsePublicKey(this.name, 'authority', params.authority) + if (params.mode !== 'createEmpty' && authority && !authority.equals(wallet.publicKey)) { throw new CCTParamsInvalidError( this.name, 'authority', diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index bb4fdae66..1a685a58c 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -4,8 +4,8 @@ import { CCIPAddressInvalidError } from '../../errors/index.ts' import { ChainFamily } from '../../networks.ts' import { CCTParamsInvalidError } from '../errors.ts' -/** Asserts `value` is a valid Solana public key string. */ -export function validatePublicKey(operation: string, param: string, value: unknown): void { +/** Parses `value` as a Solana public key or throws a CCT validation error. */ +export function parsePublicKey(operation: string, param: string, value: unknown): PublicKey { if (typeof value !== 'string') { throw new CCTParamsInvalidError( operation, @@ -15,7 +15,7 @@ export function validatePublicKey(operation: string, param: string, value: unkno } try { - new PublicKey(value) + return new PublicKey(value) } catch { throw new CCTParamsInvalidError( operation, @@ -28,6 +28,11 @@ export function validatePublicKey(operation: string, param: string, value: unkno } } +/** Asserts `value` is a valid Solana public key string. */ +export function validatePublicKey(operation: string, param: string, value: unknown): void { + parsePublicKey(operation, param, value) +} + /** Asserts ALT writable indexes are a non-empty list of byte values when provided. */ export function validateWritableIndexes( operation: string, From 8409c3e400a1244f2ed29bd6846fc5e219dfe51a Mon Sep 17 00:00:00 2001 From: mervin-link Date: Wed, 15 Jul 2026 19:34:25 +0800 Subject: [PATCH 28/87] fix: error message --- .../token-admin-registry/operations/create-lookup-table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 33e1c26ba..4e63208f5 100644 --- 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 @@ -181,7 +181,7 @@ export class CreateLookupTable extends SolanaOperation< throw new CCTParamsInvalidError( this.name, 'authority', - "createAndExtend requires authority to be the executing wallet. Use mode: 'createEmpty' for vault-owned ALTs.", + "createAndExtend requires authority to be the executing wallet. Use 'createEmpty' mode for vault-owned ALTs.", ) } From 1aab28c43726087b80394c65c7fde5941f4ae8de Mon Sep 17 00:00:00 2001 From: mervin-link Date: Wed, 15 Jul 2026 20:43:39 +0800 Subject: [PATCH 29/87] fix: update execute to remove duplicate validation --- .../operations/create-lookup-table.ts | 10 +++++++--- ccip-sdk/src/cct/solana/validate.ts | 11 +++-------- 2 files changed, 10 insertions(+), 11 deletions(-) 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 index 4e63208f5..1fe8171c1 100644 --- 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 @@ -21,7 +21,7 @@ import { } from '../../programs/router.ts' import { deriveTokenPoolConfigPda, deriveTokenPoolSignerPda } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' -import { parsePublicKey, validatePublicKey } from '../../validate.ts' +import { validatePublicKey } from '../../validate.ts' const MAX_ALT_ADDRESSES = 256 const EXTEND_CHUNK_SIZE = 30 @@ -176,7 +176,11 @@ export class CreateLookupTable extends SolanaOperation< if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) const payer = wallet.publicKey.toBase58() - const authority = params.authority && parsePublicKey(this.name, 'authority', params.authority) + const generateParams: GenerateCreateLookupTableParams = { ...rest, payer } + + this.validate(generateParams) + + const authority = params.authority ? new PublicKey(params.authority) : undefined if (params.mode !== 'createEmpty' && authority && !authority.equals(wallet.publicKey)) { throw new CCTParamsInvalidError( this.name, @@ -185,7 +189,7 @@ export class CreateLookupTable extends SolanaOperation< ) } - const tx = await this.generate(chain, { ...rest, payer }) + const tx = await this.buildUnsigned(chain, generateParams) const hash = await submit(chain, wallet, tx, this.name, computeUnits) return { ...hash, lookupTableAddress: tx.lookupTableAddress } } diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index 1a685a58c..bb4fdae66 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -4,8 +4,8 @@ import { CCIPAddressInvalidError } from '../../errors/index.ts' import { ChainFamily } from '../../networks.ts' import { CCTParamsInvalidError } from '../errors.ts' -/** Parses `value` as a Solana public key or throws a CCT validation error. */ -export function parsePublicKey(operation: string, param: string, value: unknown): PublicKey { +/** Asserts `value` is a valid Solana public key string. */ +export function validatePublicKey(operation: string, param: string, value: unknown): void { if (typeof value !== 'string') { throw new CCTParamsInvalidError( operation, @@ -15,7 +15,7 @@ export function parsePublicKey(operation: string, param: string, value: unknown) } try { - return new PublicKey(value) + new PublicKey(value) } catch { throw new CCTParamsInvalidError( operation, @@ -28,11 +28,6 @@ export function parsePublicKey(operation: string, param: string, value: unknown) } } -/** Asserts `value` is a valid Solana public key string. */ -export function validatePublicKey(operation: string, param: string, value: unknown): void { - parsePublicKey(operation, param, value) -} - /** Asserts ALT writable indexes are a non-empty list of byte values when provided. */ export function validateWritableIndexes( operation: string, From 657d489780bd5bc857d9cd3e9781bfeb353ef460 Mon Sep 17 00:00:00 2001 From: mervin-link Date: Wed, 15 Jul 2026 21:00:01 +0800 Subject: [PATCH 30/87] fix: add authority validation on execute --- .../operations/append-to-lookup-table.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 index 9004ba90a..a3e7c66d1 100644 --- 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 @@ -168,7 +168,11 @@ export class AppendToLookupTable extends SolanaOperation< if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) const payer = wallet.publicKey.toBase58() - if (params.authority && !new PublicKey(params.authority).equals(wallet.publicKey)) { + const generateParams: GenerateAppendToLookupTableParams = { ...rest, payer } + this.validate(generateParams) + + const authority = params.authority ? new PublicKey(params.authority) : undefined + if (authority && !authority.equals(wallet.publicKey)) { throw new CCTParamsInvalidError( this.name, 'authority', @@ -176,7 +180,7 @@ export class AppendToLookupTable extends SolanaOperation< ) } - const tx = await this.generate(chain, { ...rest, payer }) + const tx = await this.buildUnsigned(chain, generateParams) return submit(chain, wallet, tx, this.name, computeUnits) } } From 5aa72b809d6c39ddbff511af5cf03f7003d60565 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa Date: Thu, 16 Jul 2026 16:42:30 +0100 Subject: [PATCH 31/87] From 7f10ec095036e8728b8b67794dcb9b75351b672f Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:45:18 +0100 Subject: [PATCH 32/87] feat(cct-sdk): Init CCT SDK with setPool (EVM+Solana) (#273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What - [DAPP-10175](https://smartcontract-it.atlassian.net/browse/DAPP-10175): Add the CCT SDK at `@chainlink/ccip-sdk/cct` - Introduce `EVMTokenManager` with `generateUnsignedSetPool` (build unsigned tx) and `setPool` (sign + submit) - Add `setPool` operation module: validates params, encodes `setPool(localToken, pool)`, discovers `TokenAdminRegistry` via router - Add `Operation` / `EVMOperation` lifecycle abstraction (validate → encode → submit) - Add shared CCT infra: `TokenManager` base, submit action, address validations, and error types ## Why - TMEM's Set Pool flow currently builds calldata client-side via `ccip-contracts`. This adds a first-class SDK path for CCT admin ops so consumers (partners / dapp team) can build and submit txs server-side with consistent validation and error handling # Testing - CI ## Notes - https://github.com/smartcontractkit/explorer/pull/10928 vendors this SDK and wires it into our server-side endpoint - [CCT SDK Architecture & Requirements (DD)](https://docs.google.com/document/d/1-4Q-l5bc_kX5olWH3JWOT2Sl5oqaESUFlum2LQNxs4Y/edit?usp=sharing) [DAPP-10175]: https://smartcontract-it.atlassian.net/browse/DAPP-10175?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .gitignore | 4 +- ccip-sdk/package.json | 8 + ccip-sdk/src/cct/errors.ts | 102 +++++++++++++ ccip-sdk/src/cct/evm/index.test.ts | 132 ++++++++++++++++ ccip-sdk/src/cct/evm/index.ts | 96 ++++++++++++ ccip-sdk/src/cct/evm/operation.ts | 38 +++++ ccip-sdk/src/cct/evm/submit.test.ts | 141 ++++++++++++++++++ ccip-sdk/src/cct/evm/submit.ts | 86 +++++++++++ .../operations/set-pool.ts | 50 +++++++ ccip-sdk/src/cct/evm/validate.ts | 29 ++++ ccip-sdk/src/cct/operation.ts | 26 ++++ ccip-sdk/src/cct/solana/index.test.ts | 50 +++++++ ccip-sdk/src/cct/solana/index.ts | 116 ++++++++++++++ ccip-sdk/src/cct/solana/operation.test.ts | 70 +++++++++ ccip-sdk/src/cct/solana/operation.ts | 56 +++++++ ccip-sdk/src/cct/solana/programs/router.ts | 26 ++++ ccip-sdk/src/cct/solana/serialize.test.ts | 56 +++++++ ccip-sdk/src/cct/solana/serialize.ts | 48 ++++++ ccip-sdk/src/cct/solana/submit.test.ts | 74 +++++++++ ccip-sdk/src/cct/solana/submit.ts | 79 ++++++++++ .../operations/set-pool.test.ts | 86 +++++++++++ .../operations/set-pool.ts | 107 +++++++++++++ ccip-sdk/src/cct/solana/validate.test.ts | 58 +++++++ ccip-sdk/src/cct/solana/validate.ts | 51 +++++++ ccip-sdk/src/cct/token-manager.ts | 18 +++ ccip-sdk/src/errors/codes.ts | 5 + ccip-sdk/src/errors/recovery.ts | 8 + ccip-sdk/src/evm/index.ts | 15 +- 28 files changed, 1632 insertions(+), 3 deletions(-) create mode 100644 ccip-sdk/src/cct/errors.ts create mode 100644 ccip-sdk/src/cct/evm/index.test.ts create mode 100644 ccip-sdk/src/cct/evm/index.ts create mode 100644 ccip-sdk/src/cct/evm/operation.ts create mode 100644 ccip-sdk/src/cct/evm/submit.test.ts create mode 100644 ccip-sdk/src/cct/evm/submit.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.ts create mode 100644 ccip-sdk/src/cct/evm/validate.ts create mode 100644 ccip-sdk/src/cct/operation.ts create mode 100644 ccip-sdk/src/cct/solana/index.test.ts create mode 100644 ccip-sdk/src/cct/solana/index.ts create mode 100644 ccip-sdk/src/cct/solana/operation.test.ts create mode 100644 ccip-sdk/src/cct/solana/operation.ts create mode 100644 ccip-sdk/src/cct/solana/programs/router.ts create mode 100644 ccip-sdk/src/cct/solana/serialize.test.ts create mode 100644 ccip-sdk/src/cct/solana/serialize.ts create mode 100644 ccip-sdk/src/cct/solana/submit.test.ts create mode 100644 ccip-sdk/src/cct/solana/submit.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.ts create mode 100644 ccip-sdk/src/cct/solana/validate.test.ts create mode 100644 ccip-sdk/src/cct/solana/validate.ts create mode 100644 ccip-sdk/src/cct/token-manager.ts diff --git a/.gitignore b/.gitignore index 7e32b04e0..2963edda1 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 8c64c01c0..f237cbab6 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/*" }, diff --git a/ccip-sdk/src/cct/errors.ts b/ccip-sdk/src/cct/errors.ts new file mode 100644 index 000000000..f36c79a77 --- /dev/null +++ b/ccip-sdk/src/cct/errors.ts @@ -0,0 +1,102 @@ +/** + * 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 or the transaction reverts after mining. + * Pre-broadcast failures (signing/RPC) may set `isTransient: true` for network errors; + * on-chain reverts are permanent. Reverts 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 }, + }, + ) + } +} 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 000000000..cefda2256 --- /dev/null +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -0,0 +1,132 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, id } from 'ethers' + +import { EVMTokenManager } from './index.ts' +import { 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 ROUTER = '0x' + '33'.repeat(20) +const TAR = '0x' + '44'.repeat(20) + +/** Minimal EVMChain stub — only the members EVMTokenManager touches. */ +function stubChain(overrides: Partial = {}): EVMChain { + return { + provider: {} as never, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + getTokenAdminRegistryFor: (_address: string) => Promise.resolve(TAR), + ...overrides, + } as unknown as EVMChain +} + +const SET_POOL_SELECTOR = id('setPool(address,address)').slice(0, 10) +const EXPECTED_DATA = new Interface([ + 'function setPool(address localToken, address pool)', +]).encodeFunctionData('setPool', [TOKEN, POOL]) + +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('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('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, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts new file mode 100644 index 000000000..c08b69f6e --- /dev/null +++ b/ccip-sdk/src/cct/evm/index.ts @@ -0,0 +1,96 @@ +/** + * 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 { TransactionHash } from '../operation.ts' +import { TokenManager } from '../token-manager.ts' +import { type SetPoolParams, SetPool } from './token-admin-registry/operations/set-pool.ts' + +/** CCT admin operations for EVM chains, delegating each op to an operation class. */ +export class EVMTokenManager extends TokenManager { + readonly chain: EVMChain + readonly #setPool = new SetPool() + + /** Wraps the chain this manager builds and submits through. */ + 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 `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: SetPoolParams & { wallet: unknown }): Promise { + return this.#setPool.execute(this.chain, opts) + } +} + +export * from '../errors.ts' +export type { SetPoolParams } from './token-admin-registry/operations/set-pool.ts' +export type { TransactionHash } from '../operation.ts' diff --git a/ccip-sdk/src/cct/evm/operation.ts b/ccip-sdk/src/cct/evm/operation.ts new file mode 100644 index 000000000..e775ed16a --- /dev/null +++ b/ccip-sdk/src/cct/evm/operation.ts @@ -0,0 +1,38 @@ +/** + * EVM {@link Operation} lifecycle: validate → encode → submit. + * Concrete ops implement {@link EVMOperation.encode}; this base wires + * {@link generate} and {@link execute}. + * + * @packageDocumentation + */ + +import type { EVMChain } from '../../evm/index.ts' +import type { UnsignedEVMTx } from '../../evm/types.ts' +import { type TransactionHash, Operation } from '../operation.ts' +import { submit } from './submit.ts' + +/** EVM CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. */ +export abstract class EVMOperation

extends Operation< + EVMChain, + P, + UnsignedEVMTx +> { + /** 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) + const unsigned = await this.buildUnsigned(chain, params) + if (params.sender && unsigned.transactions[0]) unsigned.transactions[0].from = params.sender + return unsigned + } + + /** {@link generate}, then sign and submit via {@link submit}; returns once confirmed. */ + async execute(chain: EVMChain, params: P & { wallet: unknown }): Promise { + return submit(chain, params.wallet, await this.generate(chain, params), this.name) + } +} 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 000000000..bc95d9d4f --- /dev/null +++ b/ccip-sdk/src/cct/evm/submit.test.ts @@ -0,0 +1,141 @@ +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 } | 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 (shared CCT submit pipeline)', () => { + it('returns the hash on a successful receipt', async () => { + const result = await submit( + stubChain(), + fakeSigner({ receipt: { status: 1 } }), + UNSIGNED, + 'setPool', + ) + assert.deepEqual(result, { hash: HASH }) + }) + + 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 000000000..4020fae88 --- /dev/null +++ b/ccip-sdk/src/cct/evm/submit.ts @@ -0,0 +1,86 @@ +/** + * 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}. + * + * @packageDocumentation + */ + +import { 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' +import type { TransactionHash } from '../operation.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. + * `operation` labels logs and error context. + * @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 { + if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) + const sender = await wallet.getAddress() + chain.logger.debug(`${operation}: submitting...`) + + let response: TransactionResponse + let nonceConsumed = false + try { + let tx: TransactionRequest = { ...unsigned.transactions[0]! } + 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 + 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 { hash: response.hash } +} 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 000000000..4643d16c6 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.ts @@ -0,0 +1,50 @@ +/** + * setPool — registers a pool for a token in the TokenAdminRegistry. + * Version-independent (v1.5–v2.0 share one encoding). + * + * @packageDocumentation + */ + +import { interfaces } from '../../../../evm/const.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' + +/** Parameters for `setPool`. Zero `poolAddress` delists the token. */ +export type SetPoolParams = { + tokenAddress: string + /** A zero/empty `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 = interfaces.TokenAdminRegistry.encodeFunctionData('setPool', [ + p.tokenAddress, + p.poolAddress, + ]) + return { family: ChainFamily.EVM, transactions: [{ to, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/validate.ts b/ccip-sdk/src/cct/evm/validate.ts new file mode 100644 index 000000000..0ececb67a --- /dev/null +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -0,0 +1,29 @@ +/** + * Shared parameter validators for EVM CCT ops. + * + * @packageDocumentation + */ + +import { 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. 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): void { + 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), + }, + ) +} diff --git a/ccip-sdk/src/cct/operation.ts b/ccip-sdk/src/cct/operation.ts new file mode 100644 index 000000000..d29e9b9dd --- /dev/null +++ b/ccip-sdk/src/cct/operation.ts @@ -0,0 +1,26 @@ +/** + * 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' + +/** Confirmed on-chain hash returned by a successful CCT write. */ +export type TransactionHash = Pick + +/** + * 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: Params & { wallet: unknown }): Promise +} 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 000000000..bf43fc14b --- /dev/null +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Connection } from '@solana/web3.js' + +import { SolanaTokenManager } from './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 TokenAdminRegistry operations', () => { + const chain = stubChain() + const cct = SolanaTokenManager.fromChain(chain) + assert.equal(cct.chain, chain) + assert.equal(cct.provider, chain.connection) + assert.equal(typeof cct.generateUnsignedSetPool, 'function') + assert.equal(typeof cct.setPool, '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) + }) +}) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts new file mode 100644 index 000000000..f75849ddd --- /dev/null +++ b/ccip-sdk/src/cct/solana/index.ts @@ -0,0 +1,116 @@ +/** + * 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 { 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 ExecuteSetPoolParams, + type ExecuteSetPoolResult, + type GenerateSetPoolParams, + type GenerateSetPoolResult, + SetPool, +} from './token-admin-registry/operations/set-pool.ts' + +/** CCT admin facade for Solana. */ +export class SolanaTokenManager extends TokenManager { + readonly chain: SolanaChain + readonly #setPool = new SetPool() + + /** 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 { + return new SolanaTokenManager(await SolanaChain.fromConnection(provider, ctx)) + } + + /** Creates from an RPC URL. */ + static async fromUrl(url: string, ctx?: ChainContext): Promise { + return new SolanaTokenManager(await SolanaChain.fromUrl(url, ctx)) + } + + /** Provider of the underlying chain. */ + get provider(): Connection { + return this.chain.connection + } + + /** + * Builds unsigned Solana `setPool` instructions. + * + * The `payer` pays transaction fees. `authority` defaults to `payer`; Squads/multisig flows + * should pass the token admin/vault authority explicitly. + * + * @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 wallet must be the token admin authority. + * + * @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) + } + + /** + * Serializes an unsigned Solana CCT tx for external signing. + * + * @example + * ```ts + * 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 { TransactionHash } from '../operation.ts' +export type { SerializedSolanaTxEncoding } from './serialize.ts' +export type * from './token-admin-registry/operations/set-pool.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 000000000..363df1c53 --- /dev/null +++ b/ccip-sdk/src/cct/solana/operation.test.ts @@ -0,0 +1,70 @@ +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 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: [] }) + } +} + +const chain = { logger: console, connection: {} } as unknown as SolanaChain + +describe('SolanaOperation', () => { + 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 encoding', 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 000000000..7da08f406 --- /dev/null +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -0,0 +1,56 @@ +/** + * Solana {@link Operation} lifecycle: validate → 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 TransactionHash, 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 +} + +function withPayer

( + params: SolanaExecuteParams

, + payer: string, +): SolanaGenerateParams

{ + const { wallet: _wallet, ...rest } = params + return { ...rest, payer } as SolanaGenerateParams

+} + +/** Solana CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. */ +export abstract class SolanaOperation

extends Operation< + SolanaChain, + SolanaGenerateParams

, + UnsignedSolanaTx +> { + /** Build instructions after params have been validated. */ + protected abstract buildUnsigned( + chain: SolanaChain, + params: SolanaGenerateParams

, + ): Promise + + /** Run {@link validate} and {@link buildUnsigned}; no signing. */ + async generate(chain: SolanaChain, params: SolanaGenerateParams

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

): Promise { + const { wallet } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const unsigned = await this.generate(chain, withPayer(params, wallet.publicKey.toBase58())) + return submit(chain, wallet, unsigned, this.name) + } +} 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 000000000..f7f2477ab --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/router.ts @@ -0,0 +1,26 @@ +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] +} 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 000000000..97d78cb77 --- /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('cct/solana serialize', () => { + 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 000000000..3bb811254 --- /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 000000000..703e95155 --- /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('cct/solana submit error mapping', () => { + 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 000000000..7fab0e71b --- /dev/null +++ b/ccip-sdk/src/cct/solana/submit.ts @@ -0,0 +1,79 @@ +/** + * 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 { TransactionHash } from '../operation.ts' + +/** Signs, simulates, sends, and confirms a Solana CCT transaction. */ +export async function submit( + chain: SolanaChain, + wallet: unknown, + unsigned: UnsignedSolanaTx, + operation: string, +): Promise { + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + try { + return { hash: await simulateAndSendTxs(chain, wallet, unsigned) } + } 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/set-pool.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.test.ts new file mode 100644 index 000000000..df1412f9a --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.test.ts @@ -0,0 +1,86 @@ +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 { 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('Solana TokenAdminRegistry setPool', () => { + 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)) + }) +}) 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 000000000..af35b25f3 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.ts @@ -0,0 +1,107 @@ +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 { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + createRouterProgram, + deriveRouterConfigPda, + deriveTokenAdminRegistryPda, +} from '../../programs/router.ts' +import { validatePublicKey, 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 + +/** 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 = TransactionHash + +/** Solana TokenAdminRegistry `setPool` operation. */ +export class SetPool extends SolanaOperation { + readonly name = 'setPool' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateSetPoolParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'address', params.address) + validatePublicKey(this.name, 'poolLookupTableAddress', params.poolLookupTableAddress) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateWritableIndexes(this.name, 'writableIndexes', params.writableIndexes) + } + + /** Builds the unsigned Solana `setPool` instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateSetPoolParams, + ): Promise { + const routerAddress = await chain.getTokenAdminRegistryFor(opts.address) + const router = new PublicKey(routerAddress) + const tokenMint = new PublicKey(opts.tokenAddress) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + const lookupTable = new PublicKey(opts.poolLookupTableAddress) + + const routerProgram = createRouterProgram(chain, router, payer) + const config = deriveRouterConfigPda(router) + const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, tokenMint) + + const writableIndexes = opts.writableIndexes ?? [...DEFAULT_WRITABLE_INDEXES] + const instruction = await routerProgram.methods + .setPool(Buffer.from(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/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts new file mode 100644 index 000000000..345c1e78f --- /dev/null +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { PublicKey } from '@solana/web3.js' + +import { validatePublicKey, validateWritableIndexes } from './validate.ts' +import { CCTParamsInvalidError } from '../errors.ts' + +describe('cct/solana validate', () => { + it('accepts valid public keys', () => { + assert.doesNotThrow(() => validatePublicKey('op', 'payer', PublicKey.default.toBase58())) + }) + + 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('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 000000000..bb4fdae66 --- /dev/null +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -0,0 +1,51 @@ +import { PublicKey } from '@solana/web3.js' + +import { CCIPAddressInvalidError } from '../../errors/index.ts' +import { ChainFamily } from '../../networks.ts' +import { CCTParamsInvalidError } from '../errors.ts' + +/** Asserts `value` is a valid Solana public key string. */ +export function validatePublicKey(operation: string, param: string, value: unknown): void { + if (typeof value !== 'string') { + throw new CCTParamsInvalidError( + operation, + param, + `must be a valid Solana public key, got ${String(value)}`, + ) + } + + try { + 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 ALT writable indexes are a non-empty list of byte values when provided. */ +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()) { + if (!Number.isInteger(index) || index < 0 || index > 255) { + throw new CCTParamsInvalidError( + operation, + `${param}[${i}]`, + 'must be an integer between 0 and 255', + ) + } + } +} diff --git a/ccip-sdk/src/cct/token-manager.ts b/ccip-sdk/src/cct/token-manager.ts new file mode 100644 index 000000000..9efe7b425 --- /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 f50b6c73b..4bc71a7eb 100644 --- a/ccip-sdk/src/errors/codes.ts +++ b/ccip-sdk/src/errors/codes.ts @@ -178,6 +178,11 @@ 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', } as const /** Union type of all error codes. */ diff --git a/ccip-sdk/src/errors/recovery.ts b/ccip-sdk/src/errors/recovery.ts index 00e9a7fb5..9efbc3982 100644 --- a/ccip-sdk/src/errors/recovery.ts +++ b/ccip-sdk/src/errors/recovery.ts @@ -205,6 +205,14 @@ 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.', } /** 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 18683cc04..d5fdf0d09 100644 --- a/ccip-sdk/src/evm/index.ts +++ b/ccip-sdk/src/evm/index.ts @@ -164,7 +164,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 && @@ -178,7 +178,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, @@ -406,6 +406,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. From 0f4660157e384bc6d370dacad7e755eab13a9cc1 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:03:52 +0100 Subject: [PATCH 33/87] chore(cct-sdk): Gate subtree from public releases (#298) * chore(cct-sdk): Gate subtree from public releases * Fix line comment --- ccip-sdk/package.json | 5 ++++- ccip-sdk/tsconfig.build.dev.json | 4 ++++ ccip-sdk/tsconfig.build.json | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 ccip-sdk/tsconfig.build.dev.json diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index f237cbab6..ef99f8ffc 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -45,6 +45,7 @@ "typecheck": "tsc --noEmit", "check": "npm run lint && npm run typecheck", "build": "npm run clean && tsc -p ./tsconfig.build.json", + "build:dev": "npm run clean && tsc -p ./tsconfig.build.dev.json", "clean": "rm -rfv ./dist", "prepare": "npm run build" }, @@ -54,7 +55,9 @@ "tsconfig.json", "!**/*.test.*", "!**/__tests__", - "!**/__mocks__" + "!**/__mocks__", + "!dist/cct/**", + "!src/cct/**" ], "peerDependencies": { "viem": "^2.0.0" diff --git a/ccip-sdk/tsconfig.build.dev.json b/ccip-sdk/tsconfig.build.dev.json new file mode 100644 index 000000000..cec1e7dbb --- /dev/null +++ b/ccip-sdk/tsconfig.build.dev.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.build.json", + "exclude": ["node_modules", "**/*.test.*", "**/__tests__", "**/__mocks__"] +} diff --git a/ccip-sdk/tsconfig.build.json b/ccip-sdk/tsconfig.build.json index 8a845f9f0..eba9f19f5 100644 --- a/ccip-sdk/tsconfig.build.json +++ b/ccip-sdk/tsconfig.build.json @@ -11,6 +11,7 @@ "node_modules", "**/*.test.*", "**/__tests__", - "**/__mocks__" + "**/__mocks__", + "./src/cct" ] } From d7bae4df8bd131520f4a6d720e5ff2a868bea04f Mon Sep 17 00:00:00 2001 From: Mervin Date: Mon, 20 Jul 2026 23:21:50 +0800 Subject: [PATCH 34/87] feat(cct-sdk): Add deploy token solana op (#294) * feat: add deploy token op solana * fix: add tsdoc * fix: address comments * fix: address comments --- ccip-sdk/package.json | 4 + ccip-sdk/src/cct/solana/index.test.ts | 4 +- ccip-sdk/src/cct/solana/index.ts | 53 +++ .../token/operations/deploy-token.test.ts | 141 ++++++ .../solana/token/operations/deploy-token.ts | 375 ++++++++++++++++ .../src/cct/solana/token/operations/index.ts | 1 + package-lock.json | 404 +++++++++++++----- 7 files changed, 880 insertions(+), 102 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts create mode 100644 ccip-sdk/src/cct/solana/token/operations/deploy-token.ts create mode 100644 ccip-sdk/src/cct/solana/token/operations/index.ts diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index ef99f8ffc..5ff6886b1 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -78,6 +78,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.20.1", "@noble/hashes": "^2.2.0", diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 5a56eb706..0c3eefeeb 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -14,11 +14,13 @@ function stubChain(): SolanaChain { } describe('SolanaTokenManager (cct/solana)', () => { - it('fromChain exposes flat TokenAdminRegistry operations', () => { + 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) + assert.equal(typeof cct.generateUnsignedDeployToken, 'function') + assert.equal(typeof cct.deployToken, 'function') assert.equal(typeof cct.generateUnsignedCreateLookupTable, 'function') assert.equal(typeof cct.createLookupTable, 'function') assert.equal(typeof cct.generateUnsignedAppendToLookupTable, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 47e25703d..90ea9177d 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -12,6 +12,12 @@ import { 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 { + ExecuteDeployTokenParams, + ExecuteDeployTokenResult, + GenerateDeployTokenParams, + GenerateDeployTokenResult, +} from './token/operations/index.ts' import { type ExecuteAppendToLookupTableParams, type ExecuteAppendToLookupTableResult, @@ -63,6 +69,52 @@ export class SolanaTokenManager extends TokenManager return this.chain.connection } + /** + * Builds unsigned Solana mint creation instructions, optionally with initial supply. + * + * The `payer` defaults as mint, freeze, and metadata update authority. + * + * @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. + * + * @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 unsigned Solana pool lookup table instructions. * @@ -215,4 +267,5 @@ export class SolanaTokenManager extends TokenManager export * from '../errors.ts' export type { TransactionHash } from '../operation.ts' export type { SerializedSolanaTxEncoding } from './serialize.ts' +export type * from './token/operations/index.ts' export type * from './token-admin-registry/operations/index.ts' 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 000000000..a2a887a5a --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts @@ -0,0 +1,141 @@ +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('Solana token deployToken', () => { + 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 + }) + + 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', + ) + }) + + 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 000000000..256b57a61 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts @@ -0,0 +1,375 @@ +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 { 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 { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { submit } from '../../submit.ts' +import { 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 = TransactionHash & { + 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 +} + +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') + } + if (params.mintAuthority) validatePublicKey(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', + ) + } + if (params.preMintRecipient) + validatePublicKey(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 { + readonly name = 'deployToken' + + /** Validates mint and metadata params before any RPC. */ + protected validate(params: GenerateDeployTokenParams): void { + validateBaseParams(this.name, params) + validatePreMintParams(this.name, params) + validateMetaplexParams(this.name, params) + } + + /** Builds the unsigned Solana mint creation instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + params: GenerateDeployTokenParams, + ): Promise { + const config = resolveDeployTokenConfig(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, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const payer = wallet.publicKey.toBase58() + const externalSigner = getExternalMintAuthoritySigner(rest, payer) + if (externalSigner) { + throw new CCTParamsInvalidError( + this.name, + 'mintAuthority', + `requires additional signer: ${externalSigner}. Use generateUnsignedDeployToken and sign externally.`, + ) + } + + const tx = await this.generate(chain, { ...rest, payer }) + 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 000000000..0e62db660 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/index.ts @@ -0,0 +1 @@ +export * from './deploy-token.ts' diff --git a/package-lock.json b/package-lock.json index 2ac45eefd..7ec0c4ebb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -206,6 +206,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.20.1", "@noble/hashes": "^2.2.0", @@ -421,6 +425,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.55.1.tgz", "integrity": "sha512-GAqHl9zERhC3bbBfubwUu07G3UXO06gORvOcsiTBZB3et0s3auNUbHlYdYNp4VKa3sUZqH5AcD3OKzU/KDGXjQ==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.55.1", "@algolia/requester-browser-xhr": "5.55.1", @@ -631,6 +636,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -2563,6 +2569,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2585,6 +2592,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2694,6 +2702,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3115,6 +3124,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4557,6 +4567,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.1.tgz", "integrity": "sha512-2jRVrtzjf8LClGTHQlwlwuD3wQXRx3WEoF7XUarJ8Ou+0onV+SLtejsyfY9JLpfUh9hPhXM4pbBGkyAY4Bi3HQ==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/logger": "3.10.1", @@ -4826,6 +4837,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.10.1.tgz", "integrity": "sha512-0YtmIeoNo1fIw65LO8+/1dPgmDV86UmhMkow37gzjytuiCSQm9xob6PJy0L4kuQEMTLfUOGvkXvZr7GPrHquMA==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/mdx-loader": "3.10.1", "@docusaurus/module-type-aliases": "3.10.1", @@ -4972,6 +4984,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.10.1.tgz", "integrity": "sha512-3ojeJry9xBYdJO6qoyyzqeJFSJBVx2mXhyDzSdjwL2+URFQMf+h25gG38iswGImicK0ELjTd1EL2xzk8hf3QPw==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/logger": "3.10.1", "@docusaurus/types": "3.10.1", @@ -5017,6 +5030,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.10.1.tgz", "integrity": "sha512-cRv1X69jwaWv47waglllgZVWzeBFLhl53XT/XED/83BerVBTC5FTP8WTcVl8Z6sZOegDSwitu/wpCSPCDOT6lg==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/logger": "3.10.1", "@docusaurus/utils": "3.10.1", @@ -5150,31 +5164,10 @@ "entities": "^7.0.1" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "license": "MIT", "optional": true, "dependencies": { @@ -7314,6 +7307,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7583,6 +7577,7 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -7604,6 +7599,213 @@ "@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", + "peer": true, + "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": { + "@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": { + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" + } + }, + "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": { + "buffer": "^6.0.3" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^1.5.1", + "@solana/web3.js": "^1.72.0" + } + }, "node_modules/@microsoft/tsdoc": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", @@ -7938,9 +8140,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7957,9 +8156,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7976,9 +8172,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7995,9 +8188,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8241,9 +8431,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8264,9 +8451,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8287,9 +8471,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8310,9 +8491,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8333,9 +8511,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8356,9 +8531,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -9143,6 +9315,7 @@ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", @@ -9441,6 +9614,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -9553,7 +9727,6 @@ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "license": "MIT", - "peer": true, "dependencies": { "defer-to-connect": "^2.0.0" }, @@ -9589,6 +9762,7 @@ "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.63.1.tgz", "integrity": "sha512-hDWMjlKzc18W2E4OeV3hUP8ohRJNHPD4Wd1+AQJj8zshZyCRT0usrvnExgbNUTo/vntDqCGMzgYWbXxyaA+L4g==", "license": "MIT", + "peer": true, "peerDependencies": { "@ton/crypto": ">=3.2.0" } @@ -9672,7 +9846,6 @@ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "license": "MIT", - "peer": true, "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", @@ -10096,14 +10269,14 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -10140,6 +10313,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~8.3.0" } @@ -10167,6 +10341,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -10176,6 +10351,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -10217,7 +10393,6 @@ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -10543,6 +10718,7 @@ "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", @@ -10692,6 +10868,7 @@ "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.62.0", @@ -10840,9 +11017,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10857,9 +11031,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10874,9 +11045,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10891,9 +11059,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10908,9 +11073,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10925,9 +11087,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10942,9 +11101,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10959,9 +11115,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10976,9 +11129,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10993,9 +11143,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11035,6 +11182,40 @@ "node": ">=14.0.0" } }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", @@ -11250,6 +11431,7 @@ "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz", "integrity": "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/wevm" }, @@ -11293,6 +11475,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -11390,6 +11573,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -11454,6 +11638,7 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.55.1.tgz", "integrity": "sha512-FyaFnnsbVPtevQwqSj/SdxE3jAsSsY0BEH8IVLf9rXxEBdAhAmT6VKCVSMWoaPIHVN1Eufh/1w8q6k8URpIkWw==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.21.1", "@algolia/client-abtesting": "5.55.1", @@ -12193,6 +12378,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", @@ -12341,7 +12527,6 @@ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.6.0" } @@ -12351,7 +12536,6 @@ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "license": "MIT", - "peer": true, "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", @@ -12370,7 +12554,6 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "license": "MIT", - "peer": true, "dependencies": { "pump": "^3.0.0" }, @@ -12810,7 +12993,6 @@ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "license": "MIT", - "peer": true, "dependencies": { "mimic-response": "^1.0.0" }, @@ -13399,6 +13581,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -13718,6 +13901,7 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -14139,6 +14323,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -15162,6 +15347,7 @@ "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", "dev": true, "license": "MIT", + "peer": true, "workspaces": [ "packages/*" ], @@ -15221,6 +15407,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -16908,6 +17095,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -17588,7 +17776,6 @@ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "license": "MIT", - "peer": true, "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" @@ -18919,7 +19106,6 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -21429,7 +21615,6 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=4" } @@ -21885,7 +22070,6 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -22258,6 +22442,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -22413,7 +22598,6 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -23033,6 +23217,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -24038,6 +24223,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -24697,6 +24883,7 @@ "integrity": "sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -25184,6 +25371,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -25193,6 +25381,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -25229,6 +25418,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.80.0.tgz", "integrity": "sha512-4P+fk6oXsxY+6xSj7Euhc2sumQD8zQqCuVHoJwoyp9EchP+IUW9OESB7uHFJOKsIBQ4MQqYE84INJFqUCYNoOg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -25289,6 +25479,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" }, @@ -25366,6 +25557,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -25389,6 +25581,7 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -25564,7 +25757,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -26045,7 +26239,6 @@ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "license": "MIT", - "peer": true, "dependencies": { "lowercase-keys": "^2.0.0" }, @@ -26233,6 +26426,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz", "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==", "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^5.0.0", "immutable": "^5.1.5", @@ -26361,6 +26555,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -27607,6 +27802,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -28082,7 +28278,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.22.4", @@ -28229,6 +28426,7 @@ "integrity": "sha512-eJDEMAfxCmede22c/Jw7d0FA13ggAQv+KkwQYKYCdqI02cin6Rc9QRwbG/7XvvHWinuFejySnZVUWDtvGk3Vbg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 18" }, @@ -28241,6 +28439,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -28497,6 +28696,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.4" }, @@ -29153,6 +29353,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.2.tgz", "integrity": "sha512-sUWBWPJwWH+QHUObS4lfNaQ368Tj8NaHDBsRJcU/NmQpeOqxV5iQUT2c5nvDWi8WYR5ynF7az+PuMdc+oDLJOA==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", @@ -29699,6 +29900,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, From aea0670def92b5c7a08076d73e4a1fcb4a5ebd2c Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:43:41 +0100 Subject: [PATCH 35/87] feat(cct-sdk): Add version dispatch + Transfer Ownership (#289) * Better separation of concerns and abstractions * CCT: Add version dispatch + Transfer Ownership * Adjust with base * Address generic error types and doc examples * Fix linting * add example doc for op * linting fix --- ccip-sdk/src/cct/errors.ts | 93 ++++++++++ ccip-sdk/src/cct/evm/index.test.ts | 28 ++- ccip-sdk/src/cct/evm/index.ts | 47 ++++- .../operations/transfer-ownership.ts | 58 ++++++ .../src/cct/evm/token-pool/version.test.ts | 165 ++++++++++++++++++ ccip-sdk/src/cct/evm/token-pool/version.ts | 125 +++++++++++++ ccip-sdk/src/cct/evm/validate.ts | 3 +- ccip-sdk/src/errors/codes.ts | 2 + ccip-sdk/src/errors/recovery.ts | 4 + 9 files changed, 521 insertions(+), 4 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/version.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/version.ts diff --git a/ccip-sdk/src/cct/errors.ts b/ccip-sdk/src/cct/errors.ts index f36c79a77..8acb6fb8e 100644 --- a/ccip-sdk/src/cct/errors.ts +++ b/ccip-sdk/src/cct/errors.ts @@ -100,3 +100,96 @@ export class CCTTxNotConfirmedError extends CCIPError { ) } } + +// 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. */ + constructor(address: string, expected: string, actual: string, options?: CCIPErrorOptions) { + super( + CCIPErrorCode.CONTRACT_TYPE_INVALID, + `Expected a ${expected} contract at ${address}, got "${actual}"`, + { + ...options, + isTransient: false, + context: { ...options?.context, address, expected, actual }, + }, + ) + } +} + +/** + * 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 }, + }, + ) + } +} diff --git a/ccip-sdk/src/cct/evm/index.test.ts b/ccip-sdk/src/cct/evm/index.test.ts index cefda2256..48b505c99 100644 --- a/ccip-sdk/src/cct/evm/index.test.ts +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -7,7 +7,7 @@ import { EVMTokenManager } from './index.ts' import { CCIPWalletInvalidError } from '../../errors/index.ts' import type { EVMChain } from '../../evm/index.ts' import { ChainFamily } from '../../networks.ts' -import { CCTParamsInvalidError } from '../errors.ts' +import { CCTContractVersionUnsupportedError, CCTParamsInvalidError } from '../errors.ts' const TOKEN = '0x' + '11'.repeat(20) const POOL = '0x' + '22'.repeat(20) @@ -15,11 +15,12 @@ const ROUTER = '0x' + '33'.repeat(20) const TAR = '0x' + '44'.repeat(20) /** Minimal EVMChain stub — only the members EVMTokenManager touches. */ -function stubChain(overrides: Partial = {}): EVMChain { +function stubChain(overrides: Partial = {}, poolVersion = '1.5.1'): EVMChain { return { provider: {} as never, logger: { debug() {}, info() {}, warn() {}, error() {} }, getTokenAdminRegistryFor: (_address: string) => Promise.resolve(TAR), + typeAndVersion: (_address: string) => Promise.resolve(['BurnMintTokenPool', poolVersion]), ...overrides, } as unknown as EVMChain } @@ -28,6 +29,9 @@ const SET_POOL_SELECTOR = id('setPool(address,address)').slice(0, 10) 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]) describe('EVMTokenManager (cct/evm)', () => { describe('construction', () => { @@ -129,4 +133,24 @@ describe('EVMTokenManager (cct/evm)', () => { ) }) }) + + describe('transferOwnership', () => { + it('builds transferOwnership to the pool (floor-match across versions)', async () => { + const cct = EVMTokenManager.fromChain(stubChain({}, '1.6.1')) + const unsigned = await cct.generateUnsignedTransferOwnership({ + poolAddress: POOL, + newOwner: TOKEN, + }) + assert.equal(unsigned.transactions[0]!.to, POOL) + assert.equal(unsigned.transactions[0]!.data, EXPECTED_TRANSFER) + }) + + it('throws for an unsupported pool version', async () => { + const cct = EVMTokenManager.fromChain(stubChain({}, '1.6.0')) + await assert.rejects( + () => cct.generateUnsignedTransferOwnership({ poolAddress: POOL, newOwner: TOKEN }), + CCTContractVersionUnsupportedError, + ) + }) + }) }) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index c08b69f6e..c0dd4f4a8 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -15,13 +15,18 @@ import type { ChainFamily } from '../../networks.ts' import type { TransactionHash } from '../operation.ts' import { TokenManager } from '../token-manager.ts' import { type SetPoolParams, SetPool } from './token-admin-registry/operations/set-pool.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 readonly #setPool = new SetPool() + readonly #transferOwnership = new TransferOwnership() - /** Wraps the chain this manager builds and submits through. */ + /** Wraps an {@link EVMChain}; prefer the static factory methods. */ constructor(chain: EVMChain) { super() this.chain = chain @@ -89,6 +94,46 @@ export class EVMTokenManager extends TokenManager { setPool(opts: SetPoolParams & { wallet: unknown }): Promise { return this.#setPool.execute(this.chain, opts) } + + /** + * Builds an unsigned pool `transferOwnership` tx (for multisig / offline signing). + * @throws {@link CCTParamsInvalidError} if any param is invalid + * @throws {@link CCTContractTypeInvalidError} if the pool is not a recognised pool type + * @throws {@link CCTContractVersionUnsupportedError} if the pool version is unsupported + * @example + * ```typescript + * // build only — sign later (multisig / offline). `sender` must be the pool's current owner. + * const unsigned = await cct.generateUnsignedTransferOwnership({ + * poolAddress: '0xPool...', + * newOwner: '0xNewOwner...', + * sender: '0xPoolOwner...', + * }) + * ``` + */ + 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 CCTContractTypeInvalidError} if the pool is not a recognised pool type + * @throws {@link CCTContractVersionUnsupportedError} if the pool version is unsupported + * @throws {@link CCTTxFailedError} if the tx reverts or fails + * @example + * ```typescript + * // `wallet` signs as the pool's current owner; the new owner must later call acceptOwnership + * const { hash } = await cct.transferOwnership({ + * poolAddress: '0xPool...', + * newOwner: '0xNewOwner...', + * wallet, + * }) + * ``` + */ + transferOwnership(opts: TransferOwnershipParams & { wallet: unknown }): Promise { + return this.#transferOwnership.execute(this.chain, opts) + } } export * from '../errors.ts' 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 000000000..db33d01cc --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts @@ -0,0 +1,58 @@ +/** + * transferOwnership: proposes a new TokenPool owner (Ownable2Step; the new + * owner must later call acceptOwnership). + * + * @packageDocumentation + */ + +import { type InterfaceAbi, Interface } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' +import { TokenPoolVersion, resolveEncoder, resolveTokenPool } from '../version.ts' + +/** Parameters for {@link TransferOwnership}. */ +export interface TransferOwnershipParams { + poolAddress: string + newOwner: string + sender?: string +} + +/** Encodes `transferOwnership` calldata against the resolved pool ABI. */ +type Encoder = (abi: InterfaceAbi, params: TransferOwnershipParams) => UnsignedEVMTx + +const encodeTransferOwnership: Encoder = (abi, { newOwner, poolAddress }) => { + const data = new Interface(abi).encodeFunctionData('transferOwnership', [newOwner]) + return { family: ChainFamily.EVM, transactions: [{ to: poolAddress, data }] } +} + +/** 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 ABI. */ + protected async buildUnsigned( + chain: EVMChain, + { poolAddress, newOwner }: TransferOwnershipParams, + ): Promise { + const { version, abi } = await resolveTokenPool(chain, poolAddress) + return resolveEncoder(this.encoders, version, this.name)(abi, { poolAddress, newOwner }) + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/version.test.ts b/ccip-sdk/src/cct/evm/token-pool/version.test.ts new file mode 100644 index 000000000..11382ba28 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/version.test.ts @@ -0,0 +1,165 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { + TOKEN_POOL_ABIS, + TOKEN_POOL_TYPES, + TokenPoolVersion, + isTokenPoolType, + isTokenPoolVersion, + parseTokenPoolVersion, + resolveEncoder, + tokenPoolAbi, +} from './version.ts' +import { + CCTContractTypeInvalidError, + CCTContractVersionUnsupportedError, + CCTOperationUnsupportedError, +} from '../../errors.ts' + +const ADDR = '0x' + '11'.repeat(20) + +describe('pool types', () => { + it('lists known EVM pool types', () => { + assert.deepEqual([...TOKEN_POOL_TYPES], ['BurnMintTokenPool', 'LockReleaseTokenPool']) + }) + + it('isTokenPoolType narrows supported types and rejects others', () => { + assert.equal(isTokenPoolType('BurnMintTokenPool'), true) + assert.equal(isTokenPoolType('LockReleaseTokenPool'), true) + assert.equal(isTokenPoolType('UpgradeableLockReleaseTokenPool'), false) + assert.equal(isTokenPoolType('TokenAdminRegistry'), false) + }) +}) + +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) + 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('throws CCTContractVersionUnsupportedError for an unknown version', () => { + assert.throws( + () => + parseTokenPoolVersion({ + address: ADDR, + contractType: 'BurnMintTokenPool', + version: '1.7.0', + }), + CCTContractVersionUnsupportedError, + ) + }) +}) + +describe('TOKEN_POOL_ABIS', () => { + it('returns an array (ABI) for each supported version', () => { + assert.ok(Array.isArray(TOKEN_POOL_ABIS[TokenPoolVersion.V1_5_0])) + assert.ok(Array.isArray(TOKEN_POOL_ABIS[TokenPoolVersion.V1_5_1])) + assert.ok(Array.isArray(TOKEN_POOL_ABIS[TokenPoolVersion.V1_6_1])) + assert.ok(Array.isArray(TOKEN_POOL_ABIS[TokenPoolVersion.V2_0_0])) + }) + + it('returns distinct ABI objects for different version slots', () => { + assert.notDeepEqual( + TOKEN_POOL_ABIS[TokenPoolVersion.V1_5_0], + TOKEN_POOL_ABIS[TokenPoolVersion.V1_5_1], + ) + }) +}) + +describe('tokenPoolAbi', () => { + it('returns the exact ABI for the requested version', () => { + assert.equal( + tokenPoolAbi('BurnMintTokenPool', TokenPoolVersion.V1_5_0), + TOKEN_POOL_ABIS[TokenPoolVersion.V1_5_0], + ) + assert.equal( + tokenPoolAbi('LockReleaseTokenPool', TokenPoolVersion.V2_0_0), + TOKEN_POOL_ABIS[TokenPoolVersion.V2_0_0], + ) + }) + + it('ignores type today: both types resolve to the same ABI per version', () => { + assert.equal( + tokenPoolAbi('BurnMintTokenPool', TokenPoolVersion.V1_6_1), + tokenPoolAbi('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/version.ts b/ccip-sdk/src/cct/evm/token-pool/version.ts new file mode 100644 index 000000000..0415712c5 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/version.ts @@ -0,0 +1,125 @@ +/** + * EVM token-pool version axis for CCT: resolve on-chain pool metadata and ABI + * ({@link resolveTokenPool}), and floor-match encoders ({@link resolveEncoder}). + * + * @packageDocumentation + */ + +import type { InterfaceAbi } from 'ethers' + +import LockReleaseTokenPool_1_5 from '../../../evm/abi/LockReleaseTokenPool_1_5.ts' +import LockReleaseTokenPool_1_5_1 from '../../../evm/abi/LockReleaseTokenPool_1_5_1.ts' +import LockReleaseTokenPool_1_6_1 from '../../../evm/abi/LockReleaseTokenPool_1_6_1.ts' +import TokenPool_2_0 from '../../../evm/abi/TokenPool_2_0.ts' +import type { EVMChain } from '../../../evm/index.ts' +import { + CCTContractTypeInvalidError, + CCTContractVersionUnsupportedError, + CCTOperationUnsupportedError, +} from '../../errors.ts' + +/** Supported pool contract types; unsupported values fail in {@link parseTokenPoolVersion}. */ +export const TOKEN_POOL_TYPES = ['BurnMintTokenPool', 'LockReleaseTokenPool'] as const + +/** A supported EVM token-pool contract type. */ +export type TokenPoolType = (typeof TOKEN_POOL_TYPES)[number] + +/** Type guard for {@link TOKEN_POOL_TYPES}. */ +export function isTokenPoolType(v: string): v is TokenPoolType { + return TOKEN_POOL_TYPES.some((known) => known === v) +} + +/** + * 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, + 'BurnMintTokenPool or LockReleaseTokenPool', + contractType, + ) + if (!isTokenPoolVersion(version)) + throw new CCTContractVersionUnsupportedError(contractType, version, { context: { address } }) + return { type: contractType, version } +} + +/** Vendored pool ABIs keyed by {@link TokenPoolVersion}. + * TODO: split per type once BurnMint ABIs are imported from `@chainlink/contracts-ccip` */ +export const TOKEN_POOL_ABIS: Record = { + [TokenPoolVersion.V1_5_0]: LockReleaseTokenPool_1_5, + [TokenPoolVersion.V1_5_1]: LockReleaseTokenPool_1_5_1, + [TokenPoolVersion.V1_6_1]: LockReleaseTokenPool_1_6_1, + [TokenPoolVersion.V2_0_0]: TokenPool_2_0, +} + +/** + * Returns the pool ABI for `type` and `version`. `type` keeps call sites stable + * for a future per-type split; today only `version` selects the ABI. Never throws + * when `version` came from {@link parseTokenPoolVersion}. + */ +export function tokenPoolAbi(_type: TokenPoolType, version: TokenPoolVersion): InterfaceAbi { + return TOKEN_POOL_ABIS[version] +} + +/** + * Reads `chain.typeAndVersion(poolAddress)`, narrows the result, and attaches the + * pool ABI. Shared RPC boundary before versioned pool encoding. + * @throws the same errors as {@link parseTokenPoolVersion} + */ +export async function resolveTokenPool( + chain: EVMChain, + poolAddress: string, +): Promise<{ type: TokenPoolType; version: TokenPoolVersion; abi: InterfaceAbi }> { + const [contractType, version] = await chain.typeAndVersion(poolAddress) + const pool = parseTokenPoolVersion({ address: poolAddress, contractType, version }) + return { ...pool, abi: tokenPoolAbi(pool.type, pool.version) } +} + +/** + * 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/validate.ts b/ccip-sdk/src/cct/evm/validate.ts index 0ececb67a..1d8bbc689 100644 --- a/ccip-sdk/src/cct/evm/validate.ts +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -1,5 +1,6 @@ /** - * Shared parameter validators for EVM CCT ops. + * Shared parameter validators for EVM CCT operations. Throws + * {@link CCTParamsInvalidError} before any RPC so invalid inputs fail fast. * * @packageDocumentation */ diff --git a/ccip-sdk/src/errors/codes.ts b/ccip-sdk/src/errors/codes.ts index 4bc71a7eb..920eb6150 100644 --- a/ccip-sdk/src/errors/codes.ts +++ b/ccip-sdk/src/errors/codes.ts @@ -183,6 +183,8 @@ export const CCIPErrorCode = { 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', } as const /** Union type of all error codes. */ diff --git a/ccip-sdk/src/errors/recovery.ts b/ccip-sdk/src/errors/recovery.ts index 9efbc3982..285e188f1 100644 --- a/ccip-sdk/src/errors/recovery.ts +++ b/ccip-sdk/src/errors/recovery.ts @@ -213,6 +213,10 @@ export const DEFAULT_RECOVERY_HINTS: Partial> = { '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.', } /** Returns default recovery hint for error code, or undefined if none. */ From 8cfb8044dca1811757cef17c8807741d9ab4290b Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:45:49 +0100 Subject: [PATCH 36/87] feat(cct-sdk): Deploy CrossChainToken + contract artifacts (#297) * Better separation of concerns and abstractions * CCT: Add version dispatch + Transfer Ownership * Adjust with base * Address generic error types and doc examples * Fix linting * feat(cct-sdk): Add deploy evm token * Address PR comments * Address PR comments by fixing chain-specific types * feat(cct-sdk): Source chainlink/contracts-ccip artifacts (#303) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * Remove outdated bytecodes * feat(cct-sdk): Add CrossChainToken deployment + token versioning (#305) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * feat(cct-sdk): Add versioned Deploy Token (CCT + ERC20) * Remove outdated bytecodes * Deploy only CrossChainToken * Recover previous type changes * Address preMint specs --- ccip-sdk/src/cct/errors.ts | 7 +- .../V1_5_0/burn-mint-token-pool-and-proxy.ts | 1055 +++++++ .../lock-release-token-pool-and-proxy.ts | 1141 ++++++++ .../abi/V1_5_1/burn-mint-token-pool.ts | 1171 ++++++++ .../abi/V1_5_1/factory-burn-mint-erc20.ts | 483 +++ .../abi/V1_5_1/lock-release-token-pool.ts | 1276 ++++++++ .../abi/V1_6_1/burn-mint-token-pool.ts | 1171 ++++++++ .../abi/V1_6_1/lock-release-token-pool.ts | 1286 ++++++++ .../abi/V1_6_2/factory-burn-mint-erc20.ts | 490 ++++ .../abi/V2_0_0/burn-from-mint-token-pool.ts | 1665 +++++++++++ .../abi/V2_0_0/burn-mint-token-pool.ts | 1665 +++++++++++ .../V2_0_0/burn-with-from-mint-token-pool.ts | 1665 +++++++++++ .../artifacts/abi/V2_0_0/cross-chain-token.ts | 661 +++++ .../abi/V2_0_0/lock-release-token-pool.ts | 1673 +++++++++++ .../V2_0_0/burn-from-mint-token-pool.ts | 4 + .../bytecode/V2_0_0/burn-mint-token-pool.ts | 4 + .../V2_0_0/burn-with-from-mint-token-pool.ts | 4 + .../bytecode/V2_0_0/cross-chain-token.ts | 4 + .../V2_0_0/lock-release-token-pool.ts | 4 + ccip-sdk/src/cct/evm/index.test.ts | 26 + ccip-sdk/src/cct/evm/index.ts | 71 +- ccip-sdk/src/cct/evm/operation.ts | 46 +- ccip-sdk/src/cct/evm/submit.test.ts | 13 +- ccip-sdk/src/cct/evm/submit.ts | 20 +- .../evm/token/operations/deploy-token.test.ts | 258 ++ .../cct/evm/token/operations/deploy-token.ts | 140 + ccip-sdk/src/cct/evm/token/version.ts | 74 + ccip-sdk/src/cct/evm/validate.ts | 42 + ccip-sdk/src/cct/operation.ts | 14 +- ccip-sdk/src/selectors.ts | 12 + package-lock.json | 2581 +++++++++++++++-- package.json | 1 + 32 files changed, 18434 insertions(+), 293 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/burn-mint-token-pool-and-proxy.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/lock-release-token-pool-and-proxy.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/burn-mint-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/factory-burn-mint-erc20.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V1_5_1/lock-release-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V1_6_1/burn-mint-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V1_6_1/lock-release-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V1_6_2/factory-burn-mint-erc20.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/burn-from-mint-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/burn-mint-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/burn-with-from-mint-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/cross-chain-token.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/lock-release-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/burn-from-mint-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/burn-mint-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/burn-with-from-mint-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/cross-chain-token.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/lock-release-token-pool.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/deploy-token.test.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/deploy-token.ts create mode 100644 ccip-sdk/src/cct/evm/token/version.ts diff --git a/ccip-sdk/src/cct/errors.ts b/ccip-sdk/src/cct/errors.ts index 8acb6fb8e..54720bca2 100644 --- a/ccip-sdk/src/cct/errors.ts +++ b/ccip-sdk/src/cct/errors.ts @@ -42,9 +42,10 @@ export class CCTParamsInvalidError extends CCIPError { // Transaction submission /** - * Thrown when a CCT write fails before broadcast or the transaction reverts after mining. - * Pre-broadcast failures (signing/RPC) may set `isTransient: true` for network errors; - * on-chain reverts are permanent. Reverts include `context.txHash`. + * 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 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 000000000..e04b953f9 --- /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 000000000..4400c01b2 --- /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_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 000000000..f9be69bb3 --- /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 000000000..0dbf68f73 --- /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 000000000..9b072f870 --- /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_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 000000000..a9243ee8e --- /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 000000000..8863f833b --- /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 000000000..5df804e9c --- /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 000000000..e945540de --- /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 000000000..ab3df1049 --- /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 000000000..b5cfe27d7 --- /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 000000000..e967eebfb --- /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/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 000000000..1b888ccd2 --- /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 000000000..d45c91420 --- /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 000000000..a00f5f881 --- /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 000000000..b109a6872 --- /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 000000000..52ca7b625 --- /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/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 000000000..fbc5dcd89 --- /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 index 48b505c99..bcdf801a3 100644 --- a/ccip-sdk/src/cct/evm/index.test.ts +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -21,10 +21,25 @@ function stubChain(overrides: Partial = {}, poolVersion = '1.5.1'): EV logger: { debug() {}, info() {}, warn() {}, error() {} }, getTokenAdminRegistryFor: (_address: string) => Promise.resolve(TAR), typeAndVersion: (_address: string) => Promise.resolve(['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() { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(TOKEN), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ hash: HASH, wait: () => Promise.resolve({ status: 1 }) }), + } +} + const SET_POOL_SELECTOR = id('setPool(address,address)').slice(0, 10) const EXPECTED_DATA = new Interface([ 'function setPool(address localToken, address pool)', @@ -119,6 +134,17 @@ describe('EVMTokenManager (cct/evm)', () => { }) 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( diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index c0dd4f4a8..f0e2575a2 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -12,8 +12,10 @@ 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 { TransactionHash } from '../operation.ts' +import type { TransactionResult } from '../operation.ts' import { TokenManager } from '../token-manager.ts' +import type { DeployResult, EVMExecuteParams } from './operation.ts' +import { type DeployTokenParams, DeployToken } from './token/operations/deploy-token.ts' import { type SetPoolParams, SetPool } from './token-admin-registry/operations/set-pool.ts' import { type TransferOwnershipParams, @@ -25,6 +27,7 @@ export class EVMTokenManager extends TokenManager { readonly chain: EVMChain readonly #setPool = new SetPool() readonly #transferOwnership = new TransferOwnership() + readonly #deployToken = new DeployToken() /** Wraps an {@link EVMChain}; prefer the static factory methods. */ constructor(chain: EVMChain) { @@ -91,7 +94,7 @@ export class EVMTokenManager extends TokenManager { * }) * ``` */ - setPool(opts: SetPoolParams & { wallet: unknown }): Promise { + setPool(opts: EVMExecuteParams): Promise { return this.#setPool.execute(this.chain, opts) } @@ -100,15 +103,6 @@ export class EVMTokenManager extends TokenManager { * @throws {@link CCTParamsInvalidError} if any param is invalid * @throws {@link CCTContractTypeInvalidError} if the pool is not a recognised pool type * @throws {@link CCTContractVersionUnsupportedError} if the pool version is unsupported - * @example - * ```typescript - * // build only — sign later (multisig / offline). `sender` must be the pool's current owner. - * const unsigned = await cct.generateUnsignedTransferOwnership({ - * poolAddress: '0xPool...', - * newOwner: '0xNewOwner...', - * sender: '0xPoolOwner...', - * }) - * ``` */ generateUnsignedTransferOwnership(opts: TransferOwnershipParams): Promise { return this.#transferOwnership.generate(this.chain, opts) @@ -121,21 +115,62 @@ export class EVMTokenManager extends TokenManager { * @throws {@link CCTContractTypeInvalidError} if the pool is not a recognised pool type * @throws {@link CCTContractVersionUnsupportedError} if the pool version is unsupported * @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 }`. + * @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 - * // `wallet` signs as the pool's current owner; the new owner must later call acceptOwnership - * const { hash } = await cct.transferOwnership({ - * poolAddress: '0xPool...', - * newOwner: '0xNewOwner...', + * 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 and the newly deployed token address. + * @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 } = await cct.deployToken({ + * name: 'My Token', + * symbol: 'MTK', + * decimals: 18, + * maxSupply: 0n, + * owner: '0xOwner...', * wallet, * }) * ``` */ - transferOwnership(opts: TransferOwnershipParams & { wallet: unknown }): Promise { - return this.#transferOwnership.execute(this.chain, opts) + deployToken(opts: EVMExecuteParams): Promise { + return this.#deployToken.execute(this.chain, opts) } } export * from '../errors.ts' export type { SetPoolParams } from './token-admin-registry/operations/set-pool.ts' -export type { TransactionHash } from '../operation.ts' +export type { DeployTokenParams } from './token/operations/deploy-token.ts' +export type { DeployResult, EVMExecuteParams } from './operation.ts' +export type { TransactionResult } from '../operation.ts' diff --git a/ccip-sdk/src/cct/evm/operation.ts b/ccip-sdk/src/cct/evm/operation.ts index e775ed16a..d0f294324 100644 --- a/ccip-sdk/src/cct/evm/operation.ts +++ b/ccip-sdk/src/cct/evm/operation.ts @@ -1,21 +1,44 @@ /** * EVM {@link Operation} lifecycle: validate → encode → submit. - * Concrete ops implement {@link EVMOperation.encode}; this base wires - * {@link generate} and {@link execute}. + * Concrete ops implement {@link EVMOperation.buildUnsigned}; the base wires + * {@link generate} and {@link execute}. Ops needing more than a tx hash (e.g. a + * deployment's address) override {@link execute}, reusing {@link submit}. * * @packageDocumentation */ import type { EVMChain } from '../../evm/index.ts' import type { UnsignedEVMTx } from '../../evm/types.ts' -import { type TransactionHash, Operation } from '../operation.ts' +import { ChainFamily } from '../../networks.ts' +import { type ExecuteParams, type TransactionResult, Operation } from '../operation.ts' import { submit } from './submit.ts' +import { validateAddress } from './validate.ts' -/** EVM CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. */ +/** Assembles a contract-deployment tx (no `to`): creation bytecode + ABI-encoded ctor args. */ +export function deploymentTx(bytecode: `0x${string}`, ctorArgs: string): UnsignedEVMTx { + return { family: ChainFamily.EVM, transactions: [{ data: bytecode + ctorArgs.slice(2) }] } +} + +/** 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.). No block-explorer verification handle + * yet; it's recoverable from the init-code, so adding one later is non-breaking. + */ +export type DeployResult = TransactionResult & { contractAddress: string } + +/** + * 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) override {@link execute}. + */ export abstract class EVMOperation

extends Operation< EVMChain, P, - UnsignedEVMTx + UnsignedEVMTx, + TransactionResult > { /** Build calldata into an unsigned tx; versioned ops resolve their encoder here. */ protected abstract buildUnsigned( @@ -26,13 +49,20 @@ export abstract class EVMOperation

extends Operat /** 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 } - /** {@link generate}, then sign and submit via {@link submit}; returns once confirmed. */ - async execute(chain: EVMChain, params: P & { wallet: unknown }): Promise { - return submit(chain, params.wallet, await this.generate(chain, params), this.name) + /** {@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 } } } diff --git a/ccip-sdk/src/cct/evm/submit.test.ts b/ccip-sdk/src/cct/evm/submit.test.ts index bc95d9d4f..1303341ae 100644 --- a/ccip-sdk/src/cct/evm/submit.test.ts +++ b/ccip-sdk/src/cct/evm/submit.test.ts @@ -32,7 +32,7 @@ function stubChain(): EVMChain { * `submitError` makes both send and sign paths reject (pre-broadcast failure). */ function fakeSigner(opts: { - receipt?: { status: number } | null + receipt?: { status: number; contractAddress?: string | null } | null waitError?: Error submitError?: Error }) { @@ -54,15 +54,16 @@ function fakeSigner(opts: { } } -describe('submit (shared CCT submit pipeline)', () => { - it('returns the hash on a successful receipt', async () => { - const result = await submit( +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 } }), + fakeSigner({ receipt: { status: 1, contractAddress: null } }), UNSIGNED, 'setPool', ) - assert.deepEqual(result, { hash: HASH }) + assert.equal(response.hash, HASH) + assert.equal(receipt.status, 1) }) it('throws CCIPExecTxRevertedError (non-transient) when wait() throws CALL_EXCEPTION', async () => { diff --git a/ccip-sdk/src/cct/evm/submit.ts b/ccip-sdk/src/cct/evm/submit.ts index 4020fae88..7e0c8add1 100644 --- a/ccip-sdk/src/cct/evm/submit.ts +++ b/ccip-sdk/src/cct/evm/submit.ts @@ -1,18 +1,23 @@ /** * 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}. + * and on-chain reverts to {@link CCIPExecTxRevertedError}. Operations map the + * confirmed `{ response, receipt }` to their own result shape. * * @packageDocumentation */ -import { type TransactionRequest, type TransactionResponse, isError } from 'ethers' +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' -import type { TransactionHash } from '../operation.ts' /** Max ms to wait for one confirmation before throwing {@link CCTTxNotConfirmedError}. */ const CONFIRM_TIMEOUT_MS = 60_000 @@ -26,7 +31,8 @@ function isTransientError(error: unknown): boolean { /** * Signs and submits the first transaction in `unsigned`, then waits for one confirmation. - * `operation` labels logs and error context. + * 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 @@ -37,7 +43,7 @@ export async function submit( wallet: unknown, unsigned: UnsignedEVMTx, operation: string, -): Promise { +): Promise<{ response: TransactionResponse; receipt: TransactionReceipt }> { if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) const sender = await wallet.getAddress() chain.logger.debug(`${operation}: submitting...`) @@ -64,7 +70,7 @@ export async function submit( chain.logger.debug(`${operation}: waiting for confirmation, tx =`, response.hash) - let receipt + let receipt: TransactionReceipt | null try { receipt = await response.wait(1, CONFIRM_TIMEOUT_MS) } catch (error) { @@ -82,5 +88,5 @@ export async function submit( if (!receipt) throw new CCTTxNotConfirmedError(operation, response.hash) chain.logger.info(`${operation}: confirmed, tx =`, response.hash) - return { hash: response.hash } + return { response, receipt } } 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 000000000..e8980ca56 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-token.test.ts @@ -0,0 +1,258 @@ +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 }) + }) + + 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 000000000..dbb0fe5ed --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts @@ -0,0 +1,140 @@ +/** + * 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 type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import { + type DeployResult, + type EVMExecuteParams, + EVMOperation, + deploymentTx, +} from '../../operation.ts' +import { submit } from '../../submit.ts' +import { + validateAddress, + validateNonEmptyString, + validateUint256, + validateUint8, +} from '../../validate.ts' +import { TokenVersion, tokenArtifact } from '../version.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 }`. */ +export class DeployToken extends EVMOperation { + 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) + } + + /** Builds a deployment tx (no `to`): creation bytecode + ABI-encoded constructor args. */ + protected buildUnsigned(_chain: EVMChain, params: DeployTokenParams): UnsignedEVMTx { + // hardcoded to deploy CrossChainToken 2.0.0 + const { iface, bytecode } = tokenArtifact(TokenVersion.V2_0_0) + return deploymentTx(bytecode, encodeCrossChainToken(iface, params)) + } + + /** + * {@link generate}, then sign and submit; resolves to the tx hash and the newly + * deployed contract address (read from the mined receipt). + * @throws {@link CCTTxFailedError} if the tx mined without producing a contract address + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const { response, receipt } = await submit( + chain, + params.wallet, + await this.generate(chain, params), + 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 } + } +} diff --git a/ccip-sdk/src/cct/evm/token/version.ts b/ccip-sdk/src/cct/evm/token/version.ts new file mode 100644 index 000000000..ae164bb0d --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/version.ts @@ -0,0 +1,74 @@ +/** + * EVM token version axis for CCT. {@link TokenVersion} + {@link TOKEN_ABIS} cover every + * known token contract so read/write ops can resolve the right interface; + * {@link TOKEN_ARTIFACTS} / {@link tokenArtifact} add creation bytecode. `2.0.0` is + * `CrossChainToken`; `1.5.1` / `1.6.2` are `FactoryBurnMintERC20`. Mirrors + * `token-pool/version.ts`. + * + * @packageDocumentation + */ + +import { type InterfaceAbi, 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' + +/** + * 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] + +/** Contract ABI per {@link TokenVersion} — lets read/write ops resolve the right interface. */ +export const TOKEN_ABIS: Record = { + [TokenVersion.V1_5_1]: FACTORY_BURN_MINT_ERC20_V1_5_1_ABI, + [TokenVersion.V1_6_2]: FACTORY_BURN_MINT_ERC20_V1_6_2_ABI, + [TokenVersion.V2_0_0]: CROSS_CHAIN_TOKEN_V2_0_0_ABI, +} + +/** + * Returns the contract ABI for `version`. + * @throws {@link CCTContractVersionUnsupportedError} if `version` has no vendored ABI + */ +export function tokenAbi(version: TokenVersion): InterfaceAbi { + const abi = TOKEN_ABIS[version] + if (!abi) throw new CCTContractVersionUnsupportedError('token', version) + return abi +} + +/** A token deploy artifact: the cached constructor {@link Interface} and creation bytecode. */ +export interface TokenArtifact { + iface: Interface + bytecode: `0x${string}` +} + +/** + * Deploy artifacts (ctor {@link Interface} + creation bytecode) keyed by {@link TokenVersion}, + * built once. Only versions with vendored bytecode appear; read via {@link tokenArtifact}. + */ +export const TOKEN_ARTIFACTS: Partial> = { + [TokenVersion.V2_0_0]: { + iface: new Interface(CROSS_CHAIN_TOKEN_V2_0_0_ABI), + 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 tokenArtifact(version: TokenVersion): TokenArtifact { + const artifact = TOKEN_ARTIFACTS[version] + if (!artifact) throw new CCTContractVersionUnsupportedError('token', version) + return artifact +} diff --git a/ccip-sdk/src/cct/evm/validate.ts b/ccip-sdk/src/cct/evm/validate.ts index 1d8bbc689..279f2e2aa 100644 --- a/ccip-sdk/src/cct/evm/validate.ts +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -28,3 +28,45 @@ export function validateAddress(operation: string, param: string, value: unknown }, ) } + +/** + * 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 index d29e9b9dd..215b53eab 100644 --- a/ccip-sdk/src/cct/operation.ts +++ b/ccip-sdk/src/cct/operation.ts @@ -7,14 +7,20 @@ import type { ChainTransaction } from '../types.ts' -/** Confirmed on-chain hash returned by a successful CCT write. */ -export type TransactionHash = Pick +/** 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 { +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. */ @@ -22,5 +28,5 @@ export abstract class Operation { /** 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: Params & { wallet: unknown }): Promise + abstract execute(chain: Chain, params: ExecuteParams): Promise } diff --git a/ccip-sdk/src/selectors.ts b/ccip-sdk/src/selectors.ts index 277f3c877..340db547e 100644 --- a/ccip-sdk/src/selectors.ts +++ b/ccip-sdk/src/selectors.ts @@ -943,6 +943,12 @@ const SELECTORS: Selectors = { network_type: 'TESTNET', family: 'EVM', }, + '10323': { + selector: 9211758560309513668n, + name: 'mova-testnet', + network_type: 'TESTNET', + family: 'EVM', + }, '11124': { selector: 16235373811196386733n, name: 'abstract-testnet', @@ -1234,6 +1240,12 @@ const SELECTORS: Selectors = { deprecated: true, family: 'EVM', }, + '61900': { + selector: 3314641565992046393n, + name: 'mova-mainnet', + network_type: 'MAINNET', + family: 'EVM', + }, '68414': { selector: 12657445206920369324n, name: 'nexon-mainnet-henesys', diff --git a/package-lock.json b/package-lock.json index 7ec0c4ebb..3dd06df8f 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.0.1", "c8": "^11.0.0", @@ -608,6 +609,28 @@ "node": ">=20.0.0" } }, + "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", @@ -2358,6 +2381,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 @@ -2370,6 +2400,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", @@ -2381,205 +2520,759 @@ "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/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/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "^5.0.1" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "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/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", - "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" + "bin": { + "prettier": "bin-prettier.js" }, "engines": { - "node": ">=10" + "node": ">=10.13.0" }, - "peerDependencies": { - "@solana/web3.js": "^1.68.0" + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "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/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", "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "node": ">=8" } }, - "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/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": ">=18" + "node": ">= 4.0.0" } }, - "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/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", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "dependencies": { + "@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/@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/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": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" + "@changesets/types": "^6.1.0" + } + }, + "node_modules/@changesets/cli": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.31.0.tgz", + "integrity": "sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "bin": { + "changeset": "bin.js" } }, - "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": [ - { - "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", - "peer": true, + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, "engines": { "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "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": ">=6 <7 || >=8" + } + }, + "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", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "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": { + "quansync": "^0.2.7" + } + }, + "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/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": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "funding": [ { @@ -5713,17 +6406,116 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { @@ -5771,6 +6563,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", @@ -5907,10 +6724,275 @@ "@ethersproject/bytes": "^5.8.0" } }, - "node_modules/@ethersproject/bignumber": { + "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", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "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", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "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/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "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/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", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "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-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@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", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "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", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "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" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "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/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", - "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz", + "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==", + "dev": true, "funding": [ { "type": "individual", @@ -5922,16 +7004,16 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@ethersproject/bytes": "^5.8.0", - "@ethersproject/logger": "^5.8.0", - "bn.js": "^5.2.1" + "@ethersproject/sha2": "^5.8.0" } }, - "node_modules/@ethersproject/bytes": { + "node_modules/@ethersproject/properties": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", - "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", "funding": [ { "type": "individual", @@ -5947,10 +7029,11 @@ "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethersproject/constants": { + "node_modules/@ethersproject/providers": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", - "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.8.0.tgz", + "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==", + "dev": true, "funding": [ { "type": "individual", @@ -5963,13 +7046,33 @@ ], "license": "MIT", "dependencies": { - "@ethersproject/bignumber": "^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/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/hash": { + "node_modules/@ethersproject/random": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", - "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.8.0.tgz", + "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==", + "dev": true, "funding": [ { "type": "individual", @@ -5982,21 +7085,14 @@ ], "license": "MIT", "dependencies": { - "@ethersproject/abstract-signer": "^5.8.0", - "@ethersproject/address": "^5.8.0", - "@ethersproject/base64": "^5.8.0", - "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", - "@ethersproject/keccak256": "^5.8.0", - "@ethersproject/logger": "^5.8.0", - "@ethersproject/properties": "^5.8.0", - "@ethersproject/strings": "^5.8.0" + "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethersproject/keccak256": { + "node_modules/@ethersproject/rlp": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", - "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", "funding": [ { "type": "individual", @@ -6010,13 +7106,14 @@ "license": "MIT", "dependencies": { "@ethersproject/bytes": "^5.8.0", - "js-sha3": "0.8.0" + "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethersproject/logger": { + "node_modules/@ethersproject/sha2": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", - "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz", + "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==", + "dev": true, "funding": [ { "type": "individual", @@ -6027,12 +7124,17 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "license": "MIT" + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "hash.js": "1.1.7" + } }, - "node_modules/@ethersproject/networks": { + "node_modules/@ethersproject/signing-key": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", - "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", "funding": [ { "type": "individual", @@ -6045,13 +7147,19 @@ ], "license": "MIT", "dependencies": { - "@ethersproject/logger": "^5.8.0" + "@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" } }, - "node_modules/@ethersproject/properties": { + "node_modules/@ethersproject/solidity": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", - "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.8.0.tgz", + "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==", + "dev": true, "funding": [ { "type": "individual", @@ -6063,14 +7171,20 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "@ethersproject/logger": "^5.8.0" + "@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/rlp": { + "node_modules/@ethersproject/strings": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", - "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", "funding": [ { "type": "individual", @@ -6084,13 +7198,14 @@ "license": "MIT", "dependencies": { "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethersproject/signing-key": { + "node_modules/@ethersproject/transactions": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", - "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", "funding": [ { "type": "individual", @@ -6103,18 +7218,22 @@ ], "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", - "bn.js": "^5.2.1", - "elliptic": "6.6.1", - "hash.js": "1.1.7" + "@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", @@ -6126,16 +7245,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", @@ -6147,16 +7268,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": { @@ -6182,6 +7310,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", @@ -7508,32 +8661,200 @@ "integrity": "sha512-SYGogZJewIpfA0m4sELbcn7KlFGoKS7tUUxaCoPOJIEdneka5rokNBCQL+gwMKk6Zbzk/zy9KcQjkLAjivojVw==", "license": "Apache-2.0", "dependencies": { - "rxjs": "7.8.2", - "utility-types": "^3.10.0" + "rxjs": "7.8.2", + "utility-types": "^3.10.0" + } + }, + "node_modules/@ledgerhq/logs": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.17.0.tgz", + "integrity": "sha512-yra33g5q/AU7+PwAws+GaVpQGUuxnDREjVBnviJjcaJLVKuLzI4pnj8Bd3nY3fypM5k1yZEYKEXfUuGFUjP2+w==", + "license": "Apache-2.0" + }, + "node_modules/@ledgerhq/types-live": { + "version": "6.112.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/types-live/-/types-live-6.112.0.tgz", + "integrity": "sha512-FIk+CpTSo6bVlm2z1cpn7Y1MZsBxk3hK52RhUF9QL4/59MzLBC8qHfv2zCxmyJ9NJbWpEcMZFzaw7541AT96PQ==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/client-ids": "0.10.3", + "bignumber.js": "^9.1.2", + "rxjs": "7.8.2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "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/@ledgerhq/logs": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.17.0.tgz", - "integrity": "sha512-yra33g5q/AU7+PwAws+GaVpQGUuxnDREjVBnviJjcaJLVKuLzI4pnj8Bd3nY3fypM5k1yZEYKEXfUuGFUjP2+w==", - "license": "Apache-2.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/@ledgerhq/types-live": { - "version": "6.112.0", - "resolved": "https://registry.npmjs.org/@ledgerhq/types-live/-/types-live-6.112.0.tgz", - "integrity": "sha512-FIk+CpTSo6bVlm2z1cpn7Y1MZsBxk3hK52RhUF9QL4/59MzLBC8qHfv2zCxmyJ9NJbWpEcMZFzaw7541AT96PQ==", - "license": "Apache-2.0", + "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": { - "@ledgerhq/client-ids": "0.10.3", - "bignumber.js": "^9.1.2", - "rxjs": "7.8.2" + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "license": "MIT" + "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", @@ -8308,6 +9629,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.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", @@ -8955,6 +10356,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.2.0", "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", @@ -11426,6 +12834,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.2.4", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz", @@ -11689,6 +13122,16 @@ "string-width": "^4.1.0" } }, + "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", @@ -11816,6 +13259,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", @@ -11837,6 +13290,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", @@ -12033,6 +13496,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", @@ -12455,6 +13938,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", @@ -12699,6 +14192,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", @@ -12786,6 +14298,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", @@ -14493,6 +16018,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", @@ -14656,6 +16194,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", @@ -15131,6 +16679,20 @@ "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/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -16205,6 +17767,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", @@ -16570,6 +18139,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", @@ -16811,6 +18390,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", @@ -16998,6 +18587,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", @@ -17797,6 +19399,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", @@ -18348,6 +19960,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", @@ -18367,6 +19992,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", @@ -19064,6 +20699,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", @@ -19092,6 +20734,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", @@ -21794,6 +23446,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", @@ -21922,6 +23584,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", @@ -22539,6 +24208,23 @@ "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.29", "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.29.tgz", @@ -22602,6 +24288,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", @@ -22709,6 +24418,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", @@ -22918,6 +24637,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", @@ -23005,6 +24900,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", @@ -25274,6 +27179,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", @@ -25637,6 +27559,32 @@ "pify": "^2.3.0" } }, + "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", @@ -26265,6 +28213,73 @@ "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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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", @@ -26602,6 +28617,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", @@ -27312,6 +29335,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", @@ -27368,6 +29398,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", @@ -27544,6 +29585,16 @@ "node": ">=8" } }, + "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", @@ -27917,6 +29968,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.48.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", @@ -28145,6 +30209,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", @@ -28349,6 +30426,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.7.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz", diff --git a/package.json b/package.json index aa8eefcd1..f0cfaf433 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.0.1", "c8": "^11.0.0", From d4c6ae3fc6fbc8d813ad1a20ac37174940d34167 Mon Sep 17 00:00:00 2001 From: Mervin Date: Wed, 22 Jul 2026 00:10:10 +0800 Subject: [PATCH 37/87] feat(cct-sdk): Add deploy token pool solana op (#296) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * fix: address comments * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 56 +++++++ .../src/cct/solana/programs/token-pool.ts | 47 ++++++ .../operations/deploy-token-pool.test.ts | 112 +++++++++++++ .../operations/deploy-token-pool.ts | 151 ++++++++++++++++++ .../cct/solana/token-pool/operations/index.ts | 1 + ccip-sdk/src/cct/solana/validate.test.ts | 46 +++++- ccip-sdk/src/cct/solana/validate.ts | 49 +++++- 8 files changed, 461 insertions(+), 3 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/index.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 0c3eefeeb..2a3617ffe 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -21,6 +21,8 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(cct.provider, chain.connection) assert.equal(typeof cct.generateUnsignedDeployToken, 'function') assert.equal(typeof cct.deployToken, 'function') + assert.equal(typeof cct.generateUnsignedDeployTokenPool, 'function') + assert.equal(typeof cct.deployTokenPool, 'function') assert.equal(typeof cct.generateUnsignedCreateLookupTable, 'function') assert.equal(typeof cct.createLookupTable, 'function') assert.equal(typeof cct.generateUnsignedAppendToLookupTable, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 90ea9177d..dc0517175 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -35,12 +35,20 @@ import { CreateLookupTable, SetPool, } from './token-admin-registry/operations/index.ts' +import { + type ExecuteDeployTokenPoolParams, + type ExecuteDeployTokenPoolResult, + type GenerateDeployTokenPoolParams, + type GenerateDeployTokenPoolResult, + DeployTokenPool, +} from './token-pool/operations/index.ts' /** CCT admin facade for Solana. */ export class SolanaTokenManager extends TokenManager { readonly chain: SolanaChain readonly #appendToLookupTable = new AppendToLookupTable() readonly #createLookupTable = new CreateLookupTable() + readonly #deployTokenPool = new DeployTokenPool() readonly #setPool = new SetPool() /** Creates a Solana CCT manager for an existing chain. */ @@ -157,6 +165,52 @@ export class SolanaTokenManager extends TokenManager return this.#createLookupTable.execute(this.chain, opts) } + /** + * Builds unsigned Solana token pool initialize instructions. + * + * @remarks + * This only builds the pool `initialize` instruction. `authority` must be allowed to initialize + * the pool. This does not create the pool signer PDA's associated token account. + * + * @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. The signer must be allowed to initialize the + * pool. This does not create the pool signer PDA's associated token account. + * + * @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 unsigned Solana lookup table extend instructions. * @@ -265,7 +319,9 @@ export class SolanaTokenManager extends TokenManager } export * from '../errors.ts' +export { type TokenPoolType, TOKEN_POOL_PROGRAMS } from './programs/token-pool.ts' export type { TransactionHash } 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/programs/token-pool.ts b/ccip-sdk/src/cct/solana/programs/token-pool.ts index 8ae6e1064..a24e3eb2f 100644 --- a/ccip-sdk/src/cct/solana/programs/token-pool.ts +++ b/ccip-sdk/src/cct/solana/programs/token-pool.ts @@ -1,7 +1,46 @@ import { Buffer } from 'buffer' +import { Program } from '@coral-xyz/anchor' import { PublicKey } from '@solana/web3.js' +import { IDL as BASE_TOKEN_POOL_IDL } from '../../../solana/idl/1.6.0/BASE_TOKEN_POOL.ts' +import { IDL as BURN_MINT_TOKEN_POOL_IDL } from '../../../solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.ts' +import type { SolanaChain } from '../../../solana/index.ts' +import { simulationProvider } from '../../../solana/utils.ts' + +const TOKEN_POOL_IDL = { + ...BURN_MINT_TOKEN_POOL_IDL, + types: BASE_TOKEN_POOL_IDL.types, +} + +/** 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 + +/** Resolves a canonical token pool program type to its address. */ +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)) +} + +/** 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( @@ -17,3 +56,11 @@ export function deriveTokenPoolSignerPda(poolProgram: PublicKey, mint: PublicKey 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/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 000000000..ef79d9675 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.test.ts @@ -0,0 +1,112 @@ +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 } 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('Solana token pool deployTokenPool', () => { + 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(), + ) + }) + + 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)) + }) + + 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', + ) + }) + + 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 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]', + ) + }) +}) 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 000000000..97f7c48ea --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.ts @@ -0,0 +1,151 @@ +import { 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 type { TransactionHash } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type TokenPoolType, + createTokenPoolProgram, + deriveTokenPoolConfigPda, + deriveTokenPoolGlobalConfigPda, + deriveTokenPoolProgramDataPda, + resolveTokenPoolProgram, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + validateAuthorityMatchesWallet, + validatePoolType, + validatePublicKey, + validatePublicKeys, +} from '../../validate.ts' + +/** Parameters for initializing a Solana token pool, optionally with an allowlist. */ +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 + +/** Unsigned Solana token pool deploy result plus the derived pool state PDA. */ +export type GenerateDeployTokenPoolResult = UnsignedSolanaTx & { + poolAddress: string +} + +/** Parameters for executing Solana token pool deploy. */ +export type ExecuteDeployTokenPoolParams = SolanaExecuteParams + +/** Result of executing Solana token pool deploy plus the derived pool state PDA. */ +export type ExecuteDeployTokenPoolResult = TransactionHash & { + poolAddress: string +} + +/** Initializes a Solana token pool, optionally configuring an allowlist. */ +export class DeployTokenPool extends SolanaOperation< + DeployTokenPoolParams, + GenerateDeployTokenPoolResult +> { + readonly name = 'deployTokenPool' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateDeployTokenPoolParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePoolType(this.name, 'poolType', params.poolType) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + if (params.allowlist !== undefined) validatePublicKeys(this.name, 'allowlist', params.allowlist) + } + + /** Builds the unsigned Solana token pool initialize instruction set. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateDeployTokenPoolParams, + ): Promise { + const tokenMint = new PublicKey(opts.tokenAddress) + const poolProgram = resolveTokenPoolProgram(opts.poolType) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + const program = createTokenPoolProgram(chain, poolProgram, payer) + const state = deriveTokenPoolConfigPda(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(), + ] + + const allowlist = (opts.allowlist ?? []).map((a) => new PublicKey(a)) + 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() } + } + + /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ + override async execute( + chain: SolanaChain, + params: ExecuteDeployTokenPoolParams, + ): Promise { + const { wallet, computeUnits, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const payer = wallet.publicKey.toBase58() + const generateParams: GenerateDeployTokenPoolParams = { ...rest, payer } + this.validate(generateParams) + + const authority = params.authority ? new PublicKey(params.authority) : undefined + if (authority) { + validateAuthorityMatchesWallet( + this.name, + 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, generateParams) + const hash = await submit(chain, wallet, tx, this.name, computeUnits) + return { ...hash, poolAddress: tx.poolAddress } + } +} 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 000000000..f7c1f26c1 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -0,0 +1 @@ +export * from './deploy-token-pool.ts' diff --git a/ccip-sdk/src/cct/solana/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts index 345c1e78f..efcac6adf 100644 --- a/ccip-sdk/src/cct/solana/validate.test.ts +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -3,7 +3,12 @@ import { describe, it } from 'node:test' import { PublicKey } from '@solana/web3.js' -import { validatePublicKey, validateWritableIndexes } from './validate.ts' +import { + validatePoolType, + validatePublicKey, + validatePublicKeys, + validateWritableIndexes, +} from './validate.ts' import { CCTParamsInvalidError } from '../errors.ts' describe('cct/solana validate', () => { @@ -31,6 +36,45 @@ describe('cct/solana validate', () => { ) }) + it('accepts valid public key arrays', () => { + assert.doesNotThrow(() => validatePublicKeys('op', 'allowlist', [PublicKey.default.toBase58()])) + }) + + it('rejects non-array public key arrays', () => { + assert.throws( + () => validatePublicKeys('op', 'allowlist', 'nope'), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'allowlist', + ) + }) + + it('rejects invalid public key array items', () => { + assert.throws( + () => validatePublicKeys('op', 'allowlist', [PublicKey.default.toBase58(), 'nope']), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'allowlist[1]', + ) + }) + + it('accepts valid token pool types', () => { + assert.doesNotThrow(() => validatePoolType('op', 'poolType', 'burn-mint')) + assert.doesNotThrow(() => validatePoolType('op', 'poolType', 'lock-release')) + }) + + it('rejects invalid token pool types', () => { + assert.throws( + () => validatePoolType('op', 'poolType', 'mint-burn'), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'op' && + err.context.param === 'poolType', + ) + }) + it('accepts omitted and valid writable indexes', () => { assert.doesNotThrow(() => validateWritableIndexes('op', 'writableIndexes', undefined)) assert.doesNotThrow(() => validateWritableIndexes('op', 'writableIndexes', [0, 3, 255])) diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index bb4fdae66..a3beb0ba1 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -3,8 +3,12 @@ import { PublicKey } from '@solana/web3.js' import { CCIPAddressInvalidError } from '../../errors/index.ts' import { ChainFamily } from '../../networks.ts' import { CCTParamsInvalidError } from '../errors.ts' +import { type TokenPoolType, TOKEN_POOL_PROGRAMS } from './programs/token-pool.ts' -/** Asserts `value` is a valid Solana public key string. */ +/** + * 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): void { if (typeof value !== 'string') { throw new CCTParamsInvalidError( @@ -28,7 +32,48 @@ export function validatePublicKey(operation: string, param: string, value: unkno } } -/** Asserts ALT writable indexes are a non-empty list of byte values when provided. */ +/** + * 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 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') + } +} + +/** + * Asserts ALT writable indexes are a non-empty list of byte values when provided. + * @throws CCTParamsInvalidError if `writableIndexes` is invalid. + */ export function validateWritableIndexes( operation: string, param: string, From 17481c5f8cac0aefc7d7f5207575c92be575d1dc Mon Sep 17 00:00:00 2001 From: Mervin Date: Thu, 23 Jul 2026 18:54:13 +0800 Subject: [PATCH 38/87] feat(cct-sdk): Add create token account (ATA) solana op (#300) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * fix: address comments * fix: export resolveTokenMint --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 121 ++++++++++++++++-- ccip-sdk/src/cct/solana/operation.ts | 6 +- ccip-sdk/src/cct/solana/programs/alt.ts | 7 +- .../src/cct/solana/programs/token-pool.ts | 19 ++- ccip-sdk/src/cct/solana/submit.ts | 4 +- .../operations/append-to-lookup-table.test.ts | 1 - .../operations/append-to-lookup-table.ts | 5 +- .../operations/create-lookup-table.ts | 14 +- .../operations/set-pool.ts | 4 +- .../operations/deploy-token-pool.test.ts | 13 +- .../operations/deploy-token-pool.ts | 36 +++++- .../operations/create-token-account.test.ts | 83 ++++++++++++ .../token/operations/create-token-account.ts | 94 ++++++++++++++ .../solana/token/operations/deploy-token.ts | 4 +- .../src/cct/solana/token/operations/index.ts | 1 + ccip-sdk/src/solana/utils.ts | 69 +++++++--- 17 files changed, 419 insertions(+), 64 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token/operations/create-token-account.test.ts create mode 100644 ccip-sdk/src/cct/solana/token/operations/create-token-account.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 2a3617ffe..e5f8be677 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -21,6 +21,8 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(cct.provider, chain.connection) assert.equal(typeof cct.generateUnsignedDeployToken, 'function') assert.equal(typeof cct.deployToken, 'function') + assert.equal(typeof cct.generateUnsignedCreateTokenAccount, 'function') + assert.equal(typeof cct.createTokenAccount, 'function') assert.equal(typeof cct.generateUnsignedDeployTokenPool, 'function') assert.equal(typeof cct.deployTokenPool, 'function') assert.equal(typeof cct.generateUnsignedCreateLookupTable, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index dc0517175..3a4616c08 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -12,11 +12,16 @@ import { 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 { - ExecuteDeployTokenParams, - ExecuteDeployTokenResult, - GenerateDeployTokenParams, - GenerateDeployTokenResult, +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 ExecuteAppendToLookupTableParams, @@ -46,11 +51,17 @@ import { /** CCT admin facade for Solana. */ export class SolanaTokenManager extends TokenManager { readonly chain: SolanaChain + // Token operations + readonly #createTokenAccount = new CreateTokenAccount() + + // Token admin registry operations readonly #appendToLookupTable = new AppendToLookupTable() readonly #createLookupTable = new CreateLookupTable() - readonly #deployTokenPool = new DeployTokenPool() readonly #setPool = new SetPool() + // Token pool operations + readonly #deployTokenPool = new DeployTokenPool() + /** Creates a Solana CCT manager for an existing chain. */ constructor(chain: SolanaChain) { super() @@ -123,6 +134,64 @@ export class SolanaTokenManager extends TokenManager 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. + * + * @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 Solana pool lookup table instructions. * @@ -169,8 +238,14 @@ export class SolanaTokenManager extends TokenManager * Builds unsigned Solana token pool initialize instructions. * * @remarks - * This only builds the pool `initialize` instruction. `authority` must be allowed to initialize - * the pool. This does not create the pool signer PDA's associated token account. + * 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} * * @example * ```ts @@ -194,8 +269,13 @@ export class SolanaTokenManager extends TokenManager * Initializes a Solana token pool. * * @remarks - * This only sends the pool `initialize` instruction. The signer must be allowed to initialize the - * pool. This does not create the pool signer PDA's associated token account. + * 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} * * @example * ```ts @@ -263,7 +343,11 @@ export class SolanaTokenManager extends TokenManager * Builds unsigned Solana `setPool` instructions. * * The `payer` pays transaction fees. `authority` defaults to `payer`; Squads/multisig flows - * should pass the token admin/vault authority explicitly. + * 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 generateUnsignedDeployTokenPool} + * @see {@link generateUnsignedCreateTokenAccount} * * @example * ```ts @@ -282,7 +366,11 @@ export class SolanaTokenManager extends TokenManager } /** - * Registers a token pool. The wallet must be the token admin authority. + * Registers a token pool. 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 deployTokenPool} + * @see {@link createTokenAccount} * * @example * ```ts @@ -319,8 +407,13 @@ export class SolanaTokenManager extends TokenManager } export * from '../errors.ts' -export { type TokenPoolType, TOKEN_POOL_PROGRAMS } from './programs/token-pool.ts' -export type { TransactionHash } from '../operation.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' diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts index 5b870b067..3afbdfcc6 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -8,7 +8,7 @@ import { CCIPWalletInvalidError } from '../../errors/index.ts' import type { SolanaChain } from '../../solana/index.ts' import { type UnsignedSolanaTx, isWallet } from '../../solana/types.ts' -import { type TransactionHash, Operation } from '../operation.ts' +import { type TransactionResult, Operation } from '../operation.ts' import { submit } from './submit.ts' /** Unsigned Solana operation params include an explicit fee payer. */ @@ -32,7 +32,7 @@ function withPayer

( export abstract class SolanaOperation< P extends object, Tx extends UnsignedSolanaTx = UnsignedSolanaTx, -> extends Operation, Tx, TransactionHash> { +> extends Operation, Tx, TransactionResult> { /** Build instructions after params have been validated. */ protected abstract buildUnsigned(chain: SolanaChain, params: SolanaGenerateParams

): Promise @@ -43,7 +43,7 @@ export abstract class SolanaOperation< } /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ - async execute(chain: SolanaChain, params: SolanaExecuteParams

): Promise { + async execute(chain: SolanaChain, params: SolanaExecuteParams

): Promise { const { wallet, computeUnits } = params if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) diff --git a/ccip-sdk/src/cct/solana/programs/alt.ts b/ccip-sdk/src/cct/solana/programs/alt.ts index debd0a8d3..1af416002 100644 --- a/ccip-sdk/src/cct/solana/programs/alt.ts +++ b/ccip-sdk/src/cct/solana/programs/alt.ts @@ -12,7 +12,7 @@ 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 { resolveATA } from '../../../solana/utils.ts' +import { resolveTokenProgram } from '../../../solana/utils.ts' const CREATE_LOOKUP_TABLE_DISCRIMINATOR = 0 const CREATE_LOOKUP_TABLE_DATA_LENGTH = 13 @@ -21,7 +21,6 @@ type DeriveCcipLookupTableAddressesParams = { lookupTableAddress: PublicKey tokenMint: PublicKey poolProgram: PublicKey - authority: PublicKey } type BuildCreateLookupTableInstructionParams = { @@ -73,9 +72,9 @@ export function buildCreateLookupTableInstruction({ /** Derives the standard CCIP token pool addresses stored in a pool lookup table. */ export async function deriveCcipLookupTableAddresses( chain: SolanaChain, - { lookupTableAddress, tokenMint, poolProgram, authority }: DeriveCcipLookupTableAddressesParams, + { lookupTableAddress, tokenMint, poolProgram }: DeriveCcipLookupTableAddressesParams, ): Promise { - const { tokenProgram } = await resolveATA(chain.connection, tokenMint, authority) + 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) diff --git a/ccip-sdk/src/cct/solana/programs/token-pool.ts b/ccip-sdk/src/cct/solana/programs/token-pool.ts index a24e3eb2f..6385a6199 100644 --- a/ccip-sdk/src/cct/solana/programs/token-pool.ts +++ b/ccip-sdk/src/cct/solana/programs/token-pool.ts @@ -22,7 +22,14 @@ export const TOKEN_POOL_PROGRAMS = { /** Canonical Solana token pool program type. */ export type TokenPoolType = keyof typeof TOKEN_POOL_PROGRAMS -/** Resolves a canonical token pool program type to its address. */ +/** + * 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]) } @@ -49,7 +56,15 @@ export function deriveTokenPoolConfigPda(poolProgram: PublicKey, mint: PublicKey )[0] } -/** Derives a token pool signer PDA for a mint. */ +/** + * 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()], diff --git a/ccip-sdk/src/cct/solana/submit.ts b/ccip-sdk/src/cct/solana/submit.ts index 664a80f8e..d35e3a84a 100644 --- a/ccip-sdk/src/cct/solana/submit.ts +++ b/ccip-sdk/src/cct/solana/submit.ts @@ -18,7 +18,7 @@ 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 { TransactionHash } from '../operation.ts' +import type { TransactionResult } from '../operation.ts' /** Signs, simulates, sends, and confirms a Solana CCT transaction. */ export async function submit( @@ -27,7 +27,7 @@ export async function submit( unsigned: UnsignedSolanaTx, operation: string, computeUnits?: number, -): Promise { +): Promise { if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) try { 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 index bab78caab..e0f022590 100644 --- 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 @@ -89,7 +89,6 @@ describe('Solana TokenAdminRegistry appendToLookupTable', () => { lookupTableAddress: new PublicKey(LOOKUP_TABLE), tokenMint: new PublicKey(TOKEN), poolProgram: new PublicKey(POOL_PROGRAM), - authority: new PublicKey(AUTHORITY), }) await assert.rejects( 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 index a3e7c66d1..4e5983882 100644 --- 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 @@ -5,7 +5,7 @@ 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 { TransactionHash } from '../../../operation.ts' +import type { TransactionResult } from '../../../operation.ts' import { type SolanaExecuteParams, type SolanaGenerateParams, @@ -38,7 +38,7 @@ export type GenerateAppendToLookupTableResult = UnsignedSolanaTx export type ExecuteAppendToLookupTableParams = SolanaExecuteParams /** Result of executing Solana TokenAdminRegistry `appendToLookupTable`. */ -export type ExecuteAppendToLookupTableResult = TransactionHash +export type ExecuteAppendToLookupTableResult = TransactionResult /** Builds and submits Solana ALT extend instructions for token pool setup. */ export class AppendToLookupTable extends SolanaOperation< @@ -111,7 +111,6 @@ export class AppendToLookupTable extends SolanaOperation< lookupTableAddress, tokenMint, poolProgram, - authority, }) const existingAddresses = new Set( lookupTable.value.state.addresses.map((address) => address.toBase58()), 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 index 67a6da801..6ebfbc455 100644 --- 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 @@ -5,7 +5,7 @@ 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 { TransactionHash } from '../../../operation.ts' +import type { TransactionResult } from '../../../operation.ts' import { type SolanaExecuteParams, type SolanaGenerateParams, @@ -16,7 +16,7 @@ import { deriveCcipLookupTableAddresses, } from '../../programs/alt.ts' import { submit } from '../../submit.ts' -import { validatePublicKey } from '../../validate.ts' +import { validateAuthorityMatchesWallet, validatePublicKey } from '../../validate.ts' const MAX_ALT_ADDRESSES = 256 const EXTEND_CHUNK_SIZE = 30 @@ -53,7 +53,7 @@ export type GenerateCreateLookupTableResult = UnsignedSolanaTx & { export type ExecuteCreateLookupTableParams = SolanaExecuteParams /** Result of executing Solana TokenAdminRegistry `createLookupTable`. */ -export type ExecuteCreateLookupTableResult = TransactionHash & { lookupTableAddress: string } +export type ExecuteCreateLookupTableResult = TransactionResult & { lookupTableAddress: string } /** Builds and submits Solana ALT create instructions, optionally with extend instructions. */ export class CreateLookupTable extends SolanaOperation< @@ -109,7 +109,6 @@ export class CreateLookupTable extends SolanaOperation< lookupTableAddress, tokenMint, poolProgram, - authority, }) const addresses = [...ccipAddresses, ...additionalAddresses] @@ -158,10 +157,11 @@ export class CreateLookupTable extends SolanaOperation< this.validate(generateParams) const authority = params.authority ? new PublicKey(params.authority) : undefined - if (params.mode !== 'createEmpty' && authority && !authority.equals(wallet.publicKey)) { - throw new CCTParamsInvalidError( + if (params.mode !== 'createEmpty' && authority) { + validateAuthorityMatchesWallet( this.name, - 'authority', + authority, + wallet.publicKey, "createAndExtend requires authority to be the executing wallet. Use 'createEmpty' mode for vault-owned ALTs.", ) } 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 index af35b25f3..651877f5f 100644 --- 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 @@ -5,7 +5,7 @@ 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 { TransactionHash } from '../../../operation.ts' +import type { TransactionResult } from '../../../operation.ts' import { type SolanaExecuteParams, type SolanaGenerateParams, @@ -55,7 +55,7 @@ export type GenerateSetPoolResult = UnsignedSolanaTx export type ExecuteSetPoolParams = SolanaExecuteParams /** Result of executing Solana TokenAdminRegistry `setPool`. */ -export type ExecuteSetPoolResult = TransactionHash +export type ExecuteSetPoolResult = TransactionResult /** Solana TokenAdminRegistry `setPool` operation. */ export class SetPool extends SolanaOperation { 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 index ef79d9675..309e42654 100644 --- 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 @@ -6,7 +6,11 @@ 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 { + SolanaTokenManager, + deriveTokenPoolSignerPda, + resolveTokenPoolProgram, +} from '../../index.ts' import { deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' const TOKEN = Keypair.generate().publicKey.toBase58() @@ -51,6 +55,13 @@ describe('Solana token pool deployTokenPool', () => { new PublicKey(TOKEN), ).toBase58(), ) + assert.equal( + unsigned.poolSignerAddress, + deriveTokenPoolSignerPda( + resolveTokenPoolProgram('burn-mint'), + new PublicKey(TOKEN), + ).toBase58(), + ) }) it('adds configure allowlist instruction when provided', async () => { 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 index 97f7c48ea..95508b158 100644 --- 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 @@ -4,7 +4,7 @@ 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 type { TransactionHash } from '../../../operation.ts' +import type { TransactionResult } from '../../../operation.ts' import { type SolanaExecuteParams, type SolanaGenerateParams, @@ -16,6 +16,7 @@ import { deriveTokenPoolConfigPda, deriveTokenPoolGlobalConfigPda, deriveTokenPoolProgramDataPda, + deriveTokenPoolSignerPda, resolveTokenPoolProgram, } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' @@ -26,7 +27,15 @@ import { validatePublicKeys, } from '../../validate.ts' -/** Parameters for initializing a Solana token pool, optionally with an allowlist. */ +/** + * 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 @@ -45,17 +54,19 @@ type DeployTokenPoolParams = { /** Parameters for unsigned Solana token pool deploy generation. */ export type GenerateDeployTokenPoolParams = SolanaGenerateParams -/** Unsigned Solana token pool deploy result plus the derived pool state PDA. */ +/** 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 the derived pool state PDA. */ -export type ExecuteDeployTokenPoolResult = TransactionHash & { +/** 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. */ @@ -85,6 +96,7 @@ export class DeployTokenPool extends SolanaOperation< const authority = new PublicKey(opts.authority ?? opts.payer) const program = createTokenPoolProgram(chain, poolProgram, payer) const state = deriveTokenPoolConfigPda(poolProgram, tokenMint) + const poolSigner = deriveTokenPoolSignerPda(poolProgram, tokenMint) const instructions = [ await program.methods @@ -119,7 +131,13 @@ export class DeployTokenPool extends SolanaOperation< chain.logger.debug( `${this.name}: token = ${tokenMint.toBase58()}, poolProgram = ${poolProgram.toBase58()}`, ) - return { family: ChainFamily.Solana, instructions, mainIndex: 0, poolAddress: state.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. */ @@ -146,6 +164,10 @@ export class DeployTokenPool extends SolanaOperation< const tx = await this.buildUnsigned(chain, generateParams) const hash = await submit(chain, wallet, tx, this.name, computeUnits) - return { ...hash, poolAddress: tx.poolAddress } + return { + ...hash, + poolAddress: tx.poolAddress, + poolSignerAddress: tx.poolSignerAddress, + } } } 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 000000000..00fbfd59d --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/create-token-account.test.ts @@ -0,0 +1,83 @@ +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 } 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('Solana token createTokenAccount', () => { + 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(), + ) + }) + + 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, + ) + }) +}) 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 000000000..94a8defc2 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/create-token-account.ts @@ -0,0 +1,94 @@ +import { createAssociatedTokenAccountIdempotentInstruction } from '@solana/spl-token' +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 { 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 { validatePublicKey } 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 + +/** 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 +> { + readonly name = 'createTokenAccount' + + /** Validates create-token-account parameters. */ + protected validate(params: GenerateCreateTokenAccountParams): void { + validatePublicKey(this.name, 'payer', params.payer) + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'ownerAddress', params.ownerAddress) + } + + /** Builds an unsigned idempotent associated token account creation transaction. */ + protected async buildUnsigned( + chain: SolanaChain, + params: GenerateCreateTokenAccountParams, + ): Promise { + const payer = new PublicKey(params.payer) + const mint = new PublicKey(params.tokenAddress) + const owner = new PublicKey(params.ownerAddress) + 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, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const tx = await this.generate(chain, { ...rest, payer: wallet.publicKey.toBase58() }) + 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.ts b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts index 256b57a61..718bd95fe 100644 --- a/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts @@ -14,7 +14,7 @@ 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 { TransactionHash } from '../../../operation.ts' +import type { TransactionResult } from '../../../operation.ts' import { type SolanaExecuteParams, type SolanaGenerateParams, @@ -72,7 +72,7 @@ export type GenerateDeployTokenResult = UnsignedSolanaTx & { export type ExecuteDeployTokenParams = SolanaExecuteParams /** Result of executing Solana token deploy. */ -export type ExecuteDeployTokenResult = TransactionHash & { +export type ExecuteDeployTokenResult = TransactionResult & { tokenAddress: string metadataAddress?: string } diff --git a/ccip-sdk/src/cct/solana/token/operations/index.ts b/ccip-sdk/src/cct/solana/token/operations/index.ts index 0e62db660..e397c117a 100644 --- a/ccip-sdk/src/cct/solana/token/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token/operations/index.ts @@ -1 +1,2 @@ +export * from './create-token-account.ts' export * from './deploy-token.ts' diff --git a/ccip-sdk/src/solana/utils.ts b/ccip-sdk/src/solana/utils.ts index 33fb7fbaf..736c463ae 100644 --- a/ccip-sdk/src/solana/utils.ts +++ b/ccip-sdk/src/solana/utils.ts @@ -44,6 +44,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). @@ -65,22 +117,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 { From 849ed2c72e8bc664d0fcd60a76becfdc70cec947 Mon Sep 17 00:00:00 2001 From: Mervin Date: Thu, 23 Jul 2026 22:53:15 +0800 Subject: [PATCH 39/87] feat(cct-sdk): Add create token multisig solana op (#302) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * feat: add create token multisig op solana * fix: refactor validators * fix: refactor validators * fix: export pool programs and type * fix: use current module's PublicKey * fix: address comments * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * fix: change to TransactionResult * fix: address comments * fix: export resolveTokenMint * fix: merge conflicts --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 64 +++++ .../operations/create-token-multisig.test.ts | 161 ++++++++++++ .../operations/create-token-multisig.ts | 237 ++++++++++++++++++ .../cct/solana/token-pool/operations/index.ts | 1 + ccip-sdk/src/cct/solana/validate.test.ts | 48 ++-- ccip-sdk/src/cct/solana/validate.ts | 40 ++- 7 files changed, 522 insertions(+), 31 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/create-token-multisig.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/create-token-multisig.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index e5f8be677..493c50cb9 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -23,6 +23,8 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(typeof cct.deployToken, 'function') assert.equal(typeof cct.generateUnsignedCreateTokenAccount, 'function') assert.equal(typeof cct.createTokenAccount, '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.generateUnsignedCreateLookupTable, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 3a4616c08..06153a4d1 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -41,10 +41,15 @@ import { SetPool, } from './token-admin-registry/operations/index.ts' import { + type ExecuteCreateTokenMultisigParams, + type ExecuteCreateTokenMultisigResult, type ExecuteDeployTokenPoolParams, type ExecuteDeployTokenPoolResult, + type GenerateCreateTokenMultisigParams, + type GenerateCreateTokenMultisigResult, type GenerateDeployTokenPoolParams, type GenerateDeployTokenPoolResult, + CreateTokenMultisig, DeployTokenPool, } from './token-pool/operations/index.ts' @@ -60,6 +65,7 @@ export class SolanaTokenManager extends TokenManager readonly #setPool = new SetPool() // Token pool operations + readonly #createTokenMultisig = new CreateTokenMultisig() readonly #deployTokenPool = new DeployTokenPool() /** Creates a Solana CCT manager for an existing chain. */ @@ -192,6 +198,64 @@ export class SolanaTokenManager extends TokenManager 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. * 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 000000000..46456dc5d --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/create-token-multisig.test.ts @@ -0,0 +1,161 @@ +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('Solana token createTokenMultisig', () => { + 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))) + }) + + 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', + ) + }) + + 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 000000000..6b52b2ff0 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/create-token-multisig.ts @@ -0,0 +1,237 @@ +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 { 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 { 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 { + validateAuthorityMatchesWallet, + validateInteger, + validateNonEmptyString, + validatePoolType, + validatePublicKey, + validatePublicKeys, +} 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 + +/** 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 +> { + readonly name = 'createTokenMultisig' + + /** Validates public keys, threshold, and optional seed before mint/account RPCs. */ + protected validate(params: GenerateCreateTokenMultisigParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePoolType(this.name, 'poolType', params.poolType) + validatePublicKey(this.name, 'payer', params.payer) + if (params.additionalSigners !== undefined) { + validatePublicKeys(this.name, 'additionalSigners', params.additionalSigners) + } + validateInteger(this.name, 'threshold', params.threshold, 1, SOLANA_MULTISIG_MAX_SIGNERS) + if (params.seed !== undefined) validateNonEmptyString(this.name, 'seed', params.seed) + } + + /** Builds create-with-seed and initialize-multisig instructions. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateCreateTokenMultisigParams, + mintContext?: { account: MintAccount; authority: PublicKey }, + ): Promise { + const payer = new PublicKey(opts.payer) + const tokenMint = new PublicKey(opts.tokenAddress) + const poolProgram = resolveTokenPoolProgram(opts.poolType) + 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 ?? []).map((signer) => new PublicKey(signer)), + ]).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, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const generateParams: GenerateCreateTokenMultisigParams = { + ...rest, + payer: wallet.publicKey.toBase58(), + } + this.validate(generateParams) + + const tokenMint = new PublicKey(generateParams.tokenAddress) + const mintAccount = await resolveTokenMint(chain.connection, tokenMint) + + const tokenProgram = mintAccount.owner + const mintAuthority = getMintAuthority(this.name, 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, generateParams, { + 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/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index f7c1f26c1..8409dfbd7 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -1 +1,2 @@ +export * from './create-token-multisig.ts' export * from './deploy-token-pool.ts' diff --git a/ccip-sdk/src/cct/solana/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts index efcac6adf..3729a6ac6 100644 --- a/ccip-sdk/src/cct/solana/validate.test.ts +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -4,6 +4,8 @@ import { describe, it } from 'node:test' import { PublicKey } from '@solana/web3.js' import { + validateInteger, + validateNonEmptyString, validatePoolType, validatePublicKey, validatePublicKeys, @@ -36,42 +38,42 @@ describe('cct/solana validate', () => { ) }) - it('accepts valid public key arrays', () => { - assert.doesNotThrow(() => validatePublicKeys('op', 'allowlist', [PublicKey.default.toBase58()])) - }) - - it('rejects non-array public key arrays', () => { + it('validates public key arrays', () => { + assert.doesNotThrow(() => validatePublicKeys('op', 'signers', [])) + assert.doesNotThrow(() => validatePublicKeys('op', 'signers', [PublicKey.default.toBase58()])) assert.throws( - () => validatePublicKeys('op', 'allowlist', 'nope'), - (err: unknown) => - err instanceof CCTParamsInvalidError && - err.context.operation === 'op' && - err.context.param === 'allowlist', + () => 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('rejects invalid public key array items', () => { + it('validates non-empty strings', () => { + assert.doesNotThrow(() => validateNonEmptyString('op', 'seed', 'abc')) assert.throws( - () => validatePublicKeys('op', 'allowlist', [PublicKey.default.toBase58(), 'nope']), - (err: unknown) => - err instanceof CCTParamsInvalidError && - err.context.operation === 'op' && - err.context.param === 'allowlist[1]', + () => validateNonEmptyString('op', 'seed', ' '), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'seed', ) }) - it('accepts valid token pool types', () => { + 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('rejects invalid token pool types', () => { + it('validates integers', () => { + assert.doesNotThrow(() => validateInteger('op', 'threshold', 1)) + assert.doesNotThrow(() => validateInteger('op', 'decimals', 255, 0, 255)) assert.throws( - () => validatePoolType('op', 'poolType', 'mint-burn'), - (err: unknown) => - err instanceof CCTParamsInvalidError && - err.context.operation === 'op' && - err.context.param === 'poolType', + () => validateInteger('op', 'decimals', 256, 0, 255), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'decimals', ) }) diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index a3beb0ba1..02f33deea 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -41,6 +41,15 @@ export function validatePublicKeys(operation: string, param: string, values: unk 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. @@ -70,9 +79,30 @@ export function validatePoolType( } } +/** + * 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}` : '' + throw new CCTParamsInvalidError(operation, param, `must be an integer${range}`) + } +} + /** * Asserts ALT writable indexes are a non-empty list of byte values when provided. - * @throws CCTParamsInvalidError if `writableIndexes` is invalid. + * @throws CCTParamsInvalidError if indexes are empty or outside byte range. */ export function validateWritableIndexes( operation: string, @@ -85,12 +115,6 @@ export function validateWritableIndexes( } for (const [i, index] of writableIndexes.entries()) { - if (!Number.isInteger(index) || index < 0 || index > 255) { - throw new CCTParamsInvalidError( - operation, - `${param}[${i}]`, - 'must be an integer between 0 and 255', - ) - } + validateInteger(operation, `${param}[${i}]`, index, 0, 255) } } From 6281f4b73c1cc26ef455cf90a6adae846deb64ae Mon Sep 17 00:00:00 2001 From: Mervin Date: Tue, 28 Jul 2026 00:11:58 +0800 Subject: [PATCH 40/87] feat(cct-sdk): Add get token pool state config solana op (#306) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * fix: address comments --- ccip-sdk/src/cct/errors.ts | 25 +++ ccip-sdk/src/cct/solana/index.test.ts | 1 + ccip-sdk/src/cct/solana/index.ts | 26 +++ .../src/cct/solana/programs/token-pool.ts | 39 +++- ccip-sdk/src/cct/solana/query.ts | 6 + .../operations/get-token-pool-state.test.ts | 130 ++++++++++++++ .../operations/get-token-pool-state.ts | 169 ++++++++++++++++++ .../cct/solana/token-pool/operations/index.ts | 1 + ccip-sdk/src/cct/solana/validate.test.ts | 6 + ccip-sdk/src/cct/solana/validate.ts | 18 +- ccip-sdk/src/errors/codes.ts | 1 + ccip-sdk/src/errors/recovery.ts | 2 + ccip-sdk/src/solana/idl/token-pool-coder.ts | 18 ++ 13 files changed, 432 insertions(+), 10 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/query.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.ts create mode 100644 ccip-sdk/src/solana/idl/token-pool-coder.ts diff --git a/ccip-sdk/src/cct/errors.ts b/ccip-sdk/src/cct/errors.ts index 54720bca2..154e850d1 100644 --- a/ccip-sdk/src/cct/errors.ts +++ b/ccip-sdk/src/cct/errors.ts @@ -194,3 +194,28 @@ export class CCTOperationUnsupportedError extends CCIPError { ) } } + +/** + * 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(message = 'Unable to decode CCT data', options?: CCIPErrorOptions) { + super(CCIPErrorCode.CCT_DATA_DECODE_FAILED, message, { + ...options, + isTransient: false, + }) + } +} diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 493c50cb9..45fd48d2f 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -33,6 +33,7 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(typeof cct.appendToLookupTable, 'function') assert.equal(typeof cct.generateUnsignedSetPool, 'function') assert.equal(typeof cct.setPool, 'function') + assert.equal(typeof cct.getTokenPoolState, 'function') }) it('creates from a connection provider', async (t) => { diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 06153a4d1..d459825f6 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -49,8 +49,11 @@ import { type GenerateCreateTokenMultisigResult, type GenerateDeployTokenPoolParams, type GenerateDeployTokenPoolResult, + type GetTokenPoolStateParams, + type GetTokenPoolStateResult, CreateTokenMultisig, DeployTokenPool, + GetTokenPoolState, } from './token-pool/operations/index.ts' /** CCT admin facade for Solana. */ @@ -67,6 +70,7 @@ export class SolanaTokenManager extends TokenManager // Token pool operations readonly #createTokenMultisig = new CreateTokenMultisig() readonly #deployTokenPool = new DeployTokenPool() + readonly #getTokenPoolState = new GetTokenPoolState() /** Creates a Solana CCT manager for an existing chain. */ constructor(chain: SolanaChain) { @@ -451,6 +455,28 @@ export class SolanaTokenManager extends TokenManager return this.#setPool.execute(this.chain, opts) } + /** + * Reads a Burn/Mint, Lock/Release, or custom token pool's state account. + * Pass `poolProgramAddress` instead of `poolType` for a custom pool program. + * + * @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 state = await cct.getTokenPoolState({ + * poolType: 'burn-mint', + * tokenAddress: mint, + * }) + * ``` + */ + getTokenPoolState

( + opts: P, + ): Promise> { + return this.#getTokenPoolState.query(this.chain, opts) + } + /** * Serializes an unsigned Solana CCT tx for external signing. * diff --git a/ccip-sdk/src/cct/solana/programs/token-pool.ts b/ccip-sdk/src/cct/solana/programs/token-pool.ts index 6385a6199..86273f97d 100644 --- a/ccip-sdk/src/cct/solana/programs/token-pool.ts +++ b/ccip-sdk/src/cct/solana/programs/token-pool.ts @@ -3,15 +3,15 @@ import { Buffer } from 'buffer' import { Program } from '@coral-xyz/anchor' import { PublicKey } from '@solana/web3.js' -import { IDL as BASE_TOKEN_POOL_IDL } from '../../../solana/idl/1.6.0/BASE_TOKEN_POOL.ts' -import { IDL as BURN_MINT_TOKEN_POOL_IDL } from '../../../solana/idl/1.6.0/BURN_MINT_TOKEN_POOL.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' - -const TOKEN_POOL_IDL = { - ...BURN_MINT_TOKEN_POOL_IDL, - types: BASE_TOKEN_POOL_IDL.types, -} +import { CCTDataDecodeError } from '../../errors.ts' /** Canonical Solana token pool program addresses. */ export const TOKEN_POOL_PROGRAMS = { @@ -22,6 +22,12 @@ export const TOKEN_POOL_PROGRAMS = { /** Canonical Solana token pool program type. */ export type TokenPoolType = keyof typeof TOKEN_POOL_PROGRAMS +type TokenPoolStateDecodeContext = { + tokenPool: string + mint: string + poolProgram: string +} + /** * Resolves a canonical token pool program type to its address. * @@ -43,6 +49,25 @@ export function createTokenPoolProgram( 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('state', data) + } catch (cause) { + throw new CCTDataDecodeError(`Unable to decode token pool state at ${context.tokenPool}`, { + cause: cause instanceof Error ? cause : undefined, + context: { + tokenPool: context.tokenPool, + mint: context.mint, + poolProgram: context.poolProgram, + }, + }) + } +} + /** Derives the token pool global config PDA. */ export function deriveTokenPoolGlobalConfigPda(poolProgram: PublicKey): PublicKey { return PublicKey.findProgramAddressSync([Buffer.from('config')], poolProgram)[0] diff --git a/ccip-sdk/src/cct/solana/query.ts b/ccip-sdk/src/cct/solana/query.ts new file mode 100644 index 000000000..e2d76a879 --- /dev/null +++ b/ccip-sdk/src/cct/solana/query.ts @@ -0,0 +1,6 @@ +import type { SolanaChain } from '../../solana/index.ts' + +/** Shared base for read-only Solana CCT queries. */ +export abstract class SolanaQuery

{ + abstract query(chain: SolanaChain, params: P): Promise +} 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 000000000..ffa94cb59 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.test.ts @@ -0,0 +1,130 @@ +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 type { SolanaChain } from '../../../../solana/index.ts' +import { CCTDataDecodeError } from '../../../errors.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('Solana token pool getTokenPoolState', () => { + 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) + 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.match(error.message, /^Unable to decode token pool state at /) + assert.equal(error.context.mint, mint) + assert.equal(error.context.poolProgram, poolProgram) + assert.ok(error.cause instanceof Error) + 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 + }, + ) + }) + + 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 000000000..73d1e65ae --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.ts @@ -0,0 +1,169 @@ +import type { PublicKey } from '@solana/web3.js' + +import { CCIPTokenPoolStateNotFoundError } from '../../../../errors/index.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { + type TokenPoolConfig, + decodeTokenPoolState, + deriveTokenPoolConfigPda, + resolveTokenPoolProgram, +} from '../../programs/token-pool.ts' +import { SolanaQuery } from '../../query.ts' +import { parsePublicKey, validatePoolType } from '../../validate.ts' + +/** 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 + +/** 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 + 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. */ +export type GetTokenPoolStateResult

= + P extends LockReleasePoolProgramRef + ? LockReleaseGetTokenPoolStateResult + : BaseGetTokenPoolStateResult + +function resolvePoolProgram(params: PoolProgramRef): PublicKey { + const hasPoolType = Object.hasOwn(params, 'poolType') + const hasPoolProgramAddress = Object.hasOwn(params, 'poolProgramAddress') + if (hasPoolType === hasPoolProgramAddress) { + throw new CCTParamsInvalidError( + 'getTokenPoolState', + 'poolType', + 'provide exactly one of poolType or poolProgramAddress', + ) + } + + if (hasPoolType) { + validatePoolType('getTokenPoolState', 'poolType', params.poolType) + return resolveTokenPoolProgram(params.poolType) + } + + return parsePublicKey('getTokenPoolState', 'poolProgramAddress', params.poolProgramAddress) +} + +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(), + } +} + +/** Reads the complete state of a Solana token pool. */ +export class GetTokenPoolState extends SolanaQuery< + GetTokenPoolStateParams, + GetTokenPoolStateResult +> { + /** Reads and serializes the token pool configuration account. */ + async query

( + chain: SolanaChain, + params: P, + ): Promise> { + const mint = parsePublicKey('getTokenPoolState', 'tokenAddress', params.tokenAddress) + const programId = resolvePoolProgram(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(), + }) + 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, + }, + } as GetTokenPoolStateResult

+ } + + return { ...result, config: baseConfig } as GetTokenPoolStateResult

+ } +} diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index 8409dfbd7..6e8f7f4e1 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -1,2 +1,3 @@ export * from './create-token-multisig.ts' export * from './deploy-token-pool.ts' +export * from './get-token-pool-state.ts' diff --git a/ccip-sdk/src/cct/solana/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts index 3729a6ac6..a7305f21b 100644 --- a/ccip-sdk/src/cct/solana/validate.test.ts +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -4,6 +4,7 @@ import { describe, it } from 'node:test' import { PublicKey } from '@solana/web3.js' import { + parsePublicKey, validateInteger, validateNonEmptyString, validatePoolType, @@ -14,6 +15,11 @@ import { import { CCTParamsInvalidError } from '../errors.ts' describe('cct/solana validate', () => { + it('parses valid public keys', () => { + const key = parsePublicKey('op', 'payer', PublicKey.default.toBase58()) + assert.ok(key.equals(PublicKey.default)) + }) + it('accepts valid public keys', () => { assert.doesNotThrow(() => validatePublicKey('op', 'payer', PublicKey.default.toBase58())) }) diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index 02f33deea..fdc33255b 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -6,10 +6,10 @@ import { CCTParamsInvalidError } from '../errors.ts' import { type TokenPoolType, TOKEN_POOL_PROGRAMS } from './programs/token-pool.ts' /** - * Asserts `value` is a valid Solana public key string. + * Parses `value` as a Solana public key. * @throws CCTParamsInvalidError if `value` is not a valid Solana public key string. */ -export function validatePublicKey(operation: string, param: string, value: unknown): void { +export function parsePublicKey(operation: string, param: string, value: unknown): PublicKey { if (typeof value !== 'string') { throw new CCTParamsInvalidError( operation, @@ -19,7 +19,7 @@ export function validatePublicKey(operation: string, param: string, value: unkno } try { - new PublicKey(value) + return new PublicKey(value) } catch { throw new CCTParamsInvalidError( operation, @@ -32,6 +32,18 @@ export function validatePublicKey(operation: string, param: string, value: unkno } } +/** + * 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 `values` is an array of valid Solana public key strings. * @throws CCTParamsInvalidError if `values` is not an array or any item is invalid. diff --git a/ccip-sdk/src/errors/codes.ts b/ccip-sdk/src/errors/codes.ts index 920eb6150..b69c8d056 100644 --- a/ccip-sdk/src/errors/codes.ts +++ b/ccip-sdk/src/errors/codes.ts @@ -185,6 +185,7 @@ export const CCIPErrorCode = { 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/recovery.ts b/ccip-sdk/src/errors/recovery.ts index 285e188f1..bab05bd55 100644 --- a/ccip-sdk/src/errors/recovery.ts +++ b/ccip-sdk/src/errors/recovery.ts @@ -217,6 +217,8 @@ export const DEFAULT_RECOVERY_HINTS: Partial> = { '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/solana/idl/token-pool-coder.ts b/ccip-sdk/src/solana/idl/token-pool-coder.ts new file mode 100644 index 000000000..088dcf96e --- /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) From c13878c43aec8641142f6c992d86c2a760270249 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:38:26 +0100 Subject: [PATCH 41/87] feat(cct-sdk): Add deploy evm token pool + version resolution (#307) * Better separation of concerns and abstractions * CCT: Add version dispatch + Transfer Ownership * Adjust with base * Address generic error types and doc examples * Fix linting * feat(cct-sdk): Add deploy evm token * Address PR comments * Address PR comments by fixing chain-specific types * feat(cct-sdk): Source chainlink/contracts-ccip artifacts (#303) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * Remove outdated bytecodes * feat(cct-sdk): Add CrossChainToken deployment + token versioning (#305) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * feat(cct-sdk): Add versioned Deploy Token (CCT + ERC20) * Remove outdated bytecodes * Deploy only CrossChainToken * Recover previous type changes * Address preMint specs * feat(cct-sdk): Add deploy evm token pool + type/version resolution * add remarks ts doc comment * Address PR comments * add lockfile to fix ci issue * Add comments --- ccip-sdk/src/cct/evm/index.test.ts | 32 +- ccip-sdk/src/cct/evm/index.ts | 73 ++++- .../operations/set-pool.ts | 2 +- .../operations/deploy-token-pool.test.ts | 283 ++++++++++++++++++ .../operations/deploy-token-pool.ts | 175 +++++++++++ .../operations/transfer-ownership.ts | 26 +- .../src/cct/evm/token-pool/version.test.ts | 110 +++++-- ccip-sdk/src/cct/evm/token-pool/version.ts | 122 +++++--- package-lock.json | 94 +++--- 9 files changed, 773 insertions(+), 144 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-pool.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-pool.ts diff --git a/ccip-sdk/src/cct/evm/index.test.ts b/ccip-sdk/src/cct/evm/index.test.ts index bcdf801a3..4e42e604f 100644 --- a/ccip-sdk/src/cct/evm/index.test.ts +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -7,7 +7,7 @@ import { EVMTokenManager } from './index.ts' import { CCIPWalletInvalidError } from '../../errors/index.ts' import type { EVMChain } from '../../evm/index.ts' import { ChainFamily } from '../../networks.ts' -import { CCTContractVersionUnsupportedError, CCTParamsInvalidError } from '../errors.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../errors.ts' const TOKEN = '0x' + '11'.repeat(20) const POOL = '0x' + '22'.repeat(20) @@ -161,21 +161,39 @@ describe('EVMTokenManager (cct/evm)', () => { }) describe('transferOwnership', () => { - it('builds transferOwnership to the pool (floor-match across versions)', async () => { - const cct = EVMTokenManager.fromChain(stubChain({}, '1.6.1')) + 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('throws for an unsupported pool version', async () => { - const cct = EVMTokenManager.fromChain(stubChain({}, '1.6.0')) + 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 }), - CCTContractVersionUnsupportedError, + cct.generateUnsignedTransferOwnership({ poolAddress: POOL, newOwner: TOKEN }), + (err: unknown) => err instanceof CCTContractTypeInvalidError, ) }) }) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index f0e2575a2..ab3c0eba5 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -17,6 +17,10 @@ import { TokenManager } from '../token-manager.ts' import type { DeployResult, EVMExecuteParams } from './operation.ts' import { type DeployTokenParams, DeployToken } from './token/operations/deploy-token.ts' import { type SetPoolParams, SetPool } from './token-admin-registry/operations/set-pool.ts' +import { + type DeployTokenPoolParams, + DeployTokenPool, +} from './token-pool/operations/deploy-token-pool.ts' import { type TransferOwnershipParams, TransferOwnership, @@ -28,6 +32,7 @@ export class EVMTokenManager extends TokenManager { readonly #setPool = new SetPool() readonly #transferOwnership = new TransferOwnership() readonly #deployToken = new DeployToken() + readonly #deployTokenPool = new DeployTokenPool() /** Wraps an {@link EVMChain}; prefer the static factory methods. */ constructor(chain: EVMChain) { @@ -99,10 +104,10 @@ export class EVMTokenManager extends TokenManager { } /** - * Builds an unsigned pool `transferOwnership` tx (for multisig / offline signing). + * 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 - * @throws {@link CCTContractTypeInvalidError} if the pool is not a recognised pool type - * @throws {@link CCTContractVersionUnsupportedError} if the pool version is unsupported */ generateUnsignedTransferOwnership(opts: TransferOwnershipParams): Promise { return this.#transferOwnership.generate(this.chain, opts) @@ -112,14 +117,68 @@ export class EVMTokenManager extends TokenManager { * 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 CCTContractTypeInvalidError} if the pool is not a recognised pool type - * @throws {@link CCTContractVersionUnsupportedError} if the pool version is unsupported * @throws {@link CCTTxFailedError} if the tx reverts or fails */ transferOwnership(opts: EVMExecuteParams): Promise { return this.#transferOwnership.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 }`. + * @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}); + * deploying the lockbox and authorizing the pool on it are not yet SDK operations. + * @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 + * and the newly deployed pool address. `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}) — neither is + * an SDK operation yet (follow-up). + * @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 } = 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 `CrossChainToken` (v2.0.0) deployment tx (for multisig / offline * signing). The deployed address is only known once mined, so it is NOT returned here — @@ -172,5 +231,9 @@ export class EVMTokenManager extends TokenManager { export * from '../errors.ts' export type { SetPoolParams } from './token-admin-registry/operations/set-pool.ts' export type { DeployTokenParams } from './token/operations/deploy-token.ts' +export type { + DeployTokenPoolParams, + DeployableTokenPoolType, +} from './token-pool/operations/deploy-token-pool.ts' export type { DeployResult, EVMExecuteParams } from './operation.ts' export type { TransactionResult } from '../operation.ts' 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 index 4643d16c6..a31f5736d 100644 --- 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 @@ -15,7 +15,7 @@ import { validateAddress } from '../../validate.ts' /** Parameters for `setPool`. Zero `poolAddress` delists the token. */ export type SetPoolParams = { tokenAddress: string - /** A zero/empty `poolAddress` delists the token from the registry. */ + /** 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 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 000000000..38a9f9bce --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-pool.test.ts @@ -0,0 +1,283 @@ +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 LOCK_BOX = '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: LOCK_BOX, + }, + 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 }) + }) + + 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 000000000..3e16b2c47 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/deploy-token-pool.ts @@ -0,0 +1,175 @@ +/** + * 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 type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.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 DeployResult, + type EVMExecuteParams, + EVMOperation, + deploymentTx, +} from '../../operation.ts' +import { submit } from '../../submit.ts' +import { validateAddress, validateUint8 } from '../../validate.ts' +import { + type TokenPoolFamily, + type TokenPoolType, + TokenPoolVersion, + getTokenPoolFamily, + getTokenPoolInterface, +} from '../version.ts' + +/** + * 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} derives from them). 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 + +/** Fields shared by every deployable token pool. */ +interface DeployTokenPoolBase { + /** 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 DeployTokenPoolBase { + type: Exclude +} + +/** + * Params for a `LockReleaseTokenPool` — the burn constructor plus `lockBox`. + * + * @remarks Partial support: `lockBox` must be an already-deployed `ERC20LockBox` for the *same* + * `token` (the pool constructor calls `lockBox.isTokenSupported(token)` and reverts otherwise). + * This SDK does not yet deploy the lockbox or authorize the pool on it — deploy the `ERC20LockBox` + * and add the pool via the lockbox's `applyAuthorizedCallerUpdates` out-of-band before the pool can + * lock/release. A `deployLockBox` op + caller-authorization are tracked as a follow-up. + */ +export interface DeployLockReleaseTokenPoolParams extends DeployTokenPoolBase { + type: 'LockReleaseTokenPool' + /** Lock-box 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 }`. */ +export class DeployTokenPool extends EVMOperation { + 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 (!Object.hasOwn(TOKEN_POOL_BYTECODE, 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') { + validateAddress(this.name, 'lockBox', params.lockBox) + if (params.lockBox === ZeroAddress) + throw new CCTParamsInvalidError(this.name, 'lockBox', 'must not be the zero address') + } + } + + /** Builds a deployment tx (no `to`): creation bytecode + ABI-encoded constructor args. */ + protected buildUnsigned(_chain: EVMChain, params: DeployTokenPoolParams): UnsignedEVMTx { + const iface = getTokenPoolInterface(params.type, TokenPoolVersion.V2_0_0) + const encode = this.encoders[getTokenPoolFamily(params.type)] + return deploymentTx(TOKEN_POOL_BYTECODE[params.type], encode(iface, params)) + } + + /** + * {@link generate}, then sign and submit; resolves to the tx hash and the newly deployed + * pool address (read from the mined receipt). + * @throws {@link CCTTxFailedError} if the tx mined without producing a contract address + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const { response, receipt } = await submit( + chain, + params.wallet, + await this.generate(chain, params), + 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 } + } +} 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 index db33d01cc..acf2f6043 100644 --- a/ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts +++ b/ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts @@ -5,27 +5,33 @@ * @packageDocumentation */ -import { type InterfaceAbi, Interface } from 'ethers' +import type { Interface } from 'ethers' import type { EVMChain } from '../../../../evm/index.ts' import type { UnsignedEVMTx } from '../../../../evm/types.ts' import { ChainFamily } from '../../../../networks.ts' import { EVMOperation } from '../../operation.ts' import { validateAddress } from '../../validate.ts' -import { TokenPoolVersion, resolveEncoder, resolveTokenPool } from '../version.ts' +import { + TokenPoolVersion, + getTokenPoolInterface, + resolveEncoder, + resolveTokenPool, +} from '../version.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 ABI. */ -type Encoder = (abi: InterfaceAbi, params: TransferOwnershipParams) => UnsignedEVMTx +/** Encodes `transferOwnership` calldata against the resolved pool {@link Interface}. */ +type Encoder = (iface: Interface, params: TransferOwnershipParams) => UnsignedEVMTx -const encodeTransferOwnership: Encoder = (abi, { newOwner, poolAddress }) => { - const data = new Interface(abi).encodeFunctionData('transferOwnership', [newOwner]) +const encodeTransferOwnership: Encoder = (iface, { newOwner, poolAddress }) => { + const data = iface.encodeFunctionData('transferOwnership', [newOwner]) return { family: ChainFamily.EVM, transactions: [{ to: poolAddress, data }] } } @@ -47,12 +53,14 @@ export class TransferOwnership extends EVMOperation { validateAddress(this.name, 'newOwner', newOwner) } - /** Reads the pool's type-and-version, then floor-matches the encoder and its ABI. */ + /** 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 { version, abi } = await resolveTokenPool(chain, poolAddress) - return resolveEncoder(this.encoders, version, this.name)(abi, { poolAddress, newOwner }) + 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-pool/version.test.ts b/ccip-sdk/src/cct/evm/token-pool/version.test.ts index 11382ba28..48ad8c854 100644 --- a/ccip-sdk/src/cct/evm/token-pool/version.test.ts +++ b/ccip-sdk/src/cct/evm/token-pool/version.test.ts @@ -1,15 +1,19 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' +import { Interface } from 'ethers' + import { - TOKEN_POOL_ABIS, + TOKEN_POOL_FAMILIES, + TOKEN_POOL_INTERFACES, TOKEN_POOL_TYPES, TokenPoolVersion, + getTokenPoolFamily, + getTokenPoolInterface, isTokenPoolType, isTokenPoolVersion, parseTokenPoolVersion, resolveEncoder, - tokenPoolAbi, } from './version.ts' import { CCTContractTypeInvalidError, @@ -20,16 +24,38 @@ import { const ADDR = '0x' + '11'.repeat(20) describe('pool types', () => { - it('lists known EVM pool types', () => { - assert.deepEqual([...TOKEN_POOL_TYPES], ['BurnMintTokenPool', 'LockReleaseTokenPool']) + 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 narrows supported types and rejects others', () => { + 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('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', () => { @@ -45,6 +71,8 @@ describe('pool versions', () => { 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) }) @@ -96,6 +124,17 @@ describe('parseTokenPoolVersion', () => { ) }) + 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( () => @@ -109,38 +148,55 @@ describe('parseTokenPoolVersion', () => { }) }) -describe('TOKEN_POOL_ABIS', () => { - it('returns an array (ABI) for each supported version', () => { - assert.ok(Array.isArray(TOKEN_POOL_ABIS[TokenPoolVersion.V1_5_0])) - assert.ok(Array.isArray(TOKEN_POOL_ABIS[TokenPoolVersion.V1_5_1])) - assert.ok(Array.isArray(TOKEN_POOL_ABIS[TokenPoolVersion.V1_6_1])) - assert.ok(Array.isArray(TOKEN_POOL_ABIS[TokenPoolVersion.V2_0_0])) +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('returns distinct ABI objects for different version slots', () => { - assert.notDeepEqual( - TOKEN_POOL_ABIS[TokenPoolVersion.V1_5_0], - TOKEN_POOL_ABIS[TokenPoolVersion.V1_5_1], + 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], ) }) -}) -describe('tokenPoolAbi', () => { - it('returns the exact ABI for the requested version', () => { - assert.equal( - tokenPoolAbi('BurnMintTokenPool', TokenPoolVersion.V1_5_0), - TOKEN_POOL_ABIS[TokenPoolVersion.V1_5_0], + 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.equal( - tokenPoolAbi('LockReleaseTokenPool', TokenPoolVersion.V2_0_0), - TOKEN_POOL_ABIS[TokenPoolVersion.V2_0_0], + assert.ok( + !TOKEN_POOL_INTERFACES.BurnMint[TokenPoolVersion.V1_5_1].hasFunction('getPreviousPool'), ) }) +}) - it('ignores type today: both types resolve to the same ABI per version', () => { +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( - tokenPoolAbi('BurnMintTokenPool', TokenPoolVersion.V1_6_1), - tokenPoolAbi('LockReleaseTokenPool', TokenPoolVersion.V1_6_1), + 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), ) }) }) diff --git a/ccip-sdk/src/cct/evm/token-pool/version.ts b/ccip-sdk/src/cct/evm/token-pool/version.ts index 0415712c5..9ac182555 100644 --- a/ccip-sdk/src/cct/evm/token-pool/version.ts +++ b/ccip-sdk/src/cct/evm/token-pool/version.ts @@ -1,37 +1,74 @@ /** - * EVM token-pool version axis for CCT: resolve on-chain pool metadata and ABI - * ({@link resolveTokenPool}), and floor-match encoders ({@link resolveEncoder}). + * EVM token-pool version axis for CCT: resolve an on-chain pool's type + version + * ({@link resolveTokenPool}), select its cached ABI ({@link getTokenPoolInterface}), and + * floor-match version-keyed encoders ({@link resolveEncoder}). * * @packageDocumentation */ -import type { InterfaceAbi } from 'ethers' +import { Interface } from 'ethers' -import LockReleaseTokenPool_1_5 from '../../../evm/abi/LockReleaseTokenPool_1_5.ts' -import LockReleaseTokenPool_1_5_1 from '../../../evm/abi/LockReleaseTokenPool_1_5_1.ts' -import LockReleaseTokenPool_1_6_1 from '../../../evm/abi/LockReleaseTokenPool_1_6_1.ts' -import TokenPool_2_0 from '../../../evm/abi/TokenPool_2_0.ts' 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' -/** Supported pool contract types; unsupported values fail in {@link parseTokenPoolVersion}. */ -export const TOKEN_POOL_TYPES = ['BurnMintTokenPool', 'LockReleaseTokenPool'] as const +/** + * 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] /** Type guard for {@link TOKEN_POOL_TYPES}. */ export function isTokenPoolType(v: string): v is TokenPoolType { - return TOKEN_POOL_TYPES.some((known) => known === v) + 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' } /** - * Known pool versions, low to high. Value order drives floor-match in - * {@link resolveEncoder}. + * Known pool versions, low to high. Value order drives floor-match in {@link resolveEncoder}. */ export const TokenPoolVersion = { V1_5_0: '1.5.0', @@ -64,46 +101,53 @@ export function parseTokenPoolVersion({ version: string }): { type: TokenPoolType; version: TokenPoolVersion } { if (!isTokenPoolType(contractType)) - throw new CCTContractTypeInvalidError( - address, - 'BurnMintTokenPool or LockReleaseTokenPool', - contractType, - ) + throw new CCTContractTypeInvalidError(address, TOKEN_POOL_TYPES.join(', '), contractType) if (!isTokenPoolVersion(version)) throw new CCTContractVersionUnsupportedError(contractType, version, { context: { address } }) return { type: contractType, version } } -/** Vendored pool ABIs keyed by {@link TokenPoolVersion}. - * TODO: split per type once BurnMint ABIs are imported from `@chainlink/contracts-ccip` */ -export const TOKEN_POOL_ABIS: Record = { - [TokenPoolVersion.V1_5_0]: LockReleaseTokenPool_1_5, - [TokenPoolVersion.V1_5_1]: LockReleaseTokenPool_1_5_1, - [TokenPoolVersion.V1_6_1]: LockReleaseTokenPool_1_6_1, - [TokenPoolVersion.V2_0_0]: TokenPool_2_0, +/** + * 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 }) } /** - * Returns the pool ABI for `type` and `version`. `type` keeps call sites stable - * for a future per-type split; today only `version` selects the ABI. Never throws - * when `version` came from {@link parseTokenPoolVersion}. + * 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 function tokenPoolAbi(_type: TokenPoolType, version: TokenPoolVersion): InterfaceAbi { - return TOKEN_POOL_ABIS[version] +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), + }, } /** - * Reads `chain.typeAndVersion(poolAddress)`, narrows the result, and attaches the - * pool ABI. Shared RPC boundary before versioned pool encoding. - * @throws the same errors as {@link parseTokenPoolVersion} + * 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 async function resolveTokenPool( - chain: EVMChain, - poolAddress: string, -): Promise<{ type: TokenPoolType; version: TokenPoolVersion; abi: InterfaceAbi }> { - const [contractType, version] = await chain.typeAndVersion(poolAddress) - const pool = parseTokenPoolVersion({ address: poolAddress, contractType, version }) - return { ...pool, abi: tokenPoolAbi(pool.type, pool.version) } +export function getTokenPoolInterface(type: TokenPoolType, version: TokenPoolVersion): Interface { + return TOKEN_POOL_INTERFACES[getTokenPoolFamily(type)][version] } /** diff --git a/package-lock.json b/package-lock.json index 3dd06df8f..8dad4ed73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -426,7 +426,6 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.55.1.tgz", "integrity": "sha512-GAqHl9zERhC3bbBfubwUu07G3UXO06gORvOcsiTBZB3et0s3auNUbHlYdYNp4VKa3sUZqH5AcD3OKzU/KDGXjQ==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.55.1", "@algolia/requester-browser-xhr": "5.55.1", @@ -659,7 +658,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -3262,7 +3260,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -3285,7 +3282,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3395,7 +3391,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3817,7 +3812,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -5260,7 +5254,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.1.tgz", "integrity": "sha512-2jRVrtzjf8LClGTHQlwlwuD3wQXRx3WEoF7XUarJ8Ou+0onV+SLtejsyfY9JLpfUh9hPhXM4pbBGkyAY4Bi3HQ==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/logger": "3.10.1", @@ -5530,7 +5523,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.10.1.tgz", "integrity": "sha512-0YtmIeoNo1fIw65LO8+/1dPgmDV86UmhMkow37gzjytuiCSQm9xob6PJy0L4kuQEMTLfUOGvkXvZr7GPrHquMA==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/mdx-loader": "3.10.1", "@docusaurus/module-type-aliases": "3.10.1", @@ -5677,7 +5669,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.10.1.tgz", "integrity": "sha512-3ojeJry9xBYdJO6qoyyzqeJFSJBVx2mXhyDzSdjwL2+URFQMf+h25gG38iswGImicK0ELjTd1EL2xzk8hf3QPw==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/logger": "3.10.1", "@docusaurus/types": "3.10.1", @@ -5723,7 +5714,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.10.1.tgz", "integrity": "sha512-cRv1X69jwaWv47waglllgZVWzeBFLhl53XT/XED/83BerVBTC5FTP8WTcVl8Z6sZOegDSwitu/wpCSPCDOT6lg==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/logger": "3.10.1", "@docusaurus/utils": "3.10.1", @@ -5857,6 +5847,27 @@ "entities": "^7.0.1" } }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -8460,7 +8471,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8898,7 +8908,6 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -8946,7 +8955,6 @@ "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi/-/umi-1.5.1.tgz", "integrity": "sha512-ONRv5a0kv+23AMlR8oyFBHnjVg3o3N8pUfFcV4gzbg6OgZf87zHsPWBfED3OTJqx267v1bEn6d6DABXNFq9Z3A==", "license": "MIT", - "peer": true, "dependencies": { "@metaplex-foundation/umi-options": "^1.5.1", "@metaplex-foundation/umi-public-keys": "^1.5.1", @@ -10723,7 +10731,6 @@ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", @@ -11022,7 +11029,6 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -11135,6 +11141,7 @@ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "license": "MIT", + "peer": true, "dependencies": { "defer-to-connect": "^2.0.0" }, @@ -11170,7 +11177,6 @@ "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.63.1.tgz", "integrity": "sha512-hDWMjlKzc18W2E4OeV3hUP8ohRJNHPD4Wd1+AQJj8zshZyCRT0usrvnExgbNUTo/vntDqCGMzgYWbXxyaA+L4g==", "license": "MIT", - "peer": true, "peerDependencies": { "@ton/crypto": ">=3.2.0" } @@ -11254,6 +11260,7 @@ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "license": "MIT", + "peer": true, "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", @@ -11677,14 +11684,14 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -11721,7 +11728,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~8.3.0" } @@ -11749,7 +11755,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -11759,7 +11764,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -11801,6 +11805,7 @@ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -12126,7 +12131,6 @@ "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", @@ -12276,7 +12280,6 @@ "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.62.0", @@ -12864,7 +12867,6 @@ "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz", "integrity": "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/wevm" }, @@ -12908,7 +12910,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -13006,7 +13007,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13071,7 +13071,6 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.55.1.tgz", "integrity": "sha512-FyaFnnsbVPtevQwqSj/SdxE3jAsSsY0BEH8IVLf9rXxEBdAhAmT6VKCVSMWoaPIHVN1Eufh/1w8q6k8URpIkWw==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.21.1", "@algolia/client-abtesting": "5.55.1", @@ -13861,7 +13860,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", @@ -14020,6 +14018,7 @@ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.6.0" } @@ -14029,6 +14028,7 @@ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "license": "MIT", + "peer": true, "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", @@ -14047,6 +14047,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "license": "MIT", + "peer": true, "dependencies": { "pump": "^3.0.0" }, @@ -14518,6 +14519,7 @@ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "license": "MIT", + "peer": true, "dependencies": { "mimic-response": "^1.0.0" }, @@ -15106,7 +15108,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -15426,7 +15427,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -15848,7 +15848,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -16909,7 +16908,6 @@ "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", "dev": true, "license": "MIT", - "peer": true, "workspaces": [ "packages/*" ], @@ -16969,7 +16967,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -18697,7 +18694,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -19378,6 +19374,7 @@ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "license": "MIT", + "peer": true, "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" @@ -20758,6 +20755,7 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -23267,6 +23265,7 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -23739,6 +23738,7 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -24111,7 +24111,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -24284,6 +24283,7 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -25122,7 +25122,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -26128,7 +26127,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -26788,7 +26786,6 @@ "integrity": "sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -27293,7 +27290,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -27303,7 +27299,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -27340,7 +27335,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.80.0.tgz", "integrity": "sha512-4P+fk6oXsxY+6xSj7Euhc2sumQD8zQqCuVHoJwoyp9EchP+IUW9OESB7uHFJOKsIBQ4MQqYE84INJFqUCYNoOg==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -27401,7 +27395,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" }, @@ -27479,7 +27472,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -27503,7 +27495,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -27705,8 +27696,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -28187,6 +28177,7 @@ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "license": "MIT", + "peer": true, "dependencies": { "lowercase-keys": "^2.0.0" }, @@ -28441,7 +28432,6 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz", "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==", "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^5.0.0", "immutable": "^5.1.5", @@ -28570,7 +28560,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -29853,7 +29842,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -30355,8 +30343,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.22.4", @@ -30513,7 +30500,6 @@ "integrity": "sha512-eJDEMAfxCmede22c/Jw7d0FA13ggAQv+KkwQYKYCdqI02cin6Rc9QRwbG/7XvvHWinuFejySnZVUWDtvGk3Vbg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 18" }, @@ -30526,7 +30512,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -30783,7 +30768,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.4" }, @@ -31440,7 +31424,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.2.tgz", "integrity": "sha512-sUWBWPJwWH+QHUObS4lfNaQ368Tj8NaHDBsRJcU/NmQpeOqxV5iQUT2c5nvDWi8WYR5ynF7az+PuMdc+oDLJOA==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", @@ -31987,7 +31970,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, From bcec89b5f7af814e6ad94814a60d24e5d6764581 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:02:11 +0100 Subject: [PATCH 42/87] feat(cct-sdk): Deploy Lockbox (#311) * Better separation of concerns and abstractions * CCT: Add version dispatch + Transfer Ownership * Adjust with base * Address generic error types and doc examples * Fix linting * feat(cct-sdk): Add deploy evm token * Address PR comments * Address PR comments by fixing chain-specific types * feat(cct-sdk): Source chainlink/contracts-ccip artifacts (#303) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * Remove outdated bytecodes * feat(cct-sdk): Add CrossChainToken deployment + token versioning (#305) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * feat(cct-sdk): Add versioned Deploy Token (CCT + ERC20) * Remove outdated bytecodes * Deploy only CrossChainToken * Recover previous type changes * Address preMint specs * feat(cct-sdk): Add deploy evm token pool + type/version resolution * add remarks ts doc comment * Address PR comments * add lockfile to fix ci issue * Add comments * feat(cct-sdk): Add EVM deployLockbox operation (DAPP-10738) Vendor ERC20LockBox v2.0.0 artifacts; add the cached lockbox Interface + DeployLockbox op, wire generateUnsignedDeployLockbox/deployLockbox into EVMTokenManager, and document the LockRelease deploy sequence. * Address comments and linting --- .../evm/artifacts/abi/V2_0_0/erc20-lockbox.ts | 254 ++++++++++++++++++ .../bytecode/V2_0_0/erc20-lockbox.ts | 4 + ccip-sdk/src/cct/evm/index.ts | 112 +++++--- ccip-sdk/src/cct/evm/lockbox/interface.ts | 19 ++ .../lockbox/operations/deploy-lockbox.test.ts | 148 ++++++++++ .../evm/lockbox/operations/deploy-lockbox.ts | 70 +++++ .../operations/deploy-token-pool.test.ts | 20 +- .../operations/deploy-token-pool.ts | 32 ++- 8 files changed, 600 insertions(+), 59 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V2_0_0/erc20-lockbox.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/bytecode/V2_0_0/erc20-lockbox.ts create mode 100644 ccip-sdk/src/cct/evm/lockbox/interface.ts create mode 100644 ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.test.ts create mode 100644 ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts 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 000000000..b498bff89 --- /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/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 000000000..e69f8fd59 --- /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/index.ts b/ccip-sdk/src/cct/evm/index.ts index ab3c0eba5..963e9a4a2 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -14,6 +14,7 @@ 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 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 SetPoolParams, SetPool } from './token-admin-registry/operations/set-pool.ts' @@ -33,6 +34,7 @@ export class EVMTokenManager extends TokenManager { readonly #transferOwnership = new TransferOwnership() readonly #deployToken = new DeployToken() readonly #deployTokenPool = new DeployTokenPool() + readonly #deployLockbox = new DeployLockbox() /** Wraps an {@link EVMChain}; prefer the static factory methods. */ constructor(chain: EVMChain) { @@ -123,6 +125,54 @@ export class EVMTokenManager extends TokenManager { 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 }`. + * @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 and the newly deployed token address. + * @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 } = 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`, @@ -131,13 +181,15 @@ export class EVMTokenManager extends TokenManager { * `{ hash, contractAddress }`. * @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}); - * deploying the lockbox and authorizing the pool on it are not yet SDK operations. + * 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) → `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` + * type: 'BurnMintTokenPool', // burn-* variant; LockReleaseTokenPool additionally requires `lockbox` * token: '0xToken...', * localTokenDecimals: 18, * rmnProxy: '0xRmnProxy...', @@ -157,8 +209,10 @@ export class EVMTokenManager extends TokenManager { * @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}) — neither is - * an SDK operation yet (follow-up). + * `lockbox` and the pool authorized on it ({@link DeployLockReleaseTokenPoolParams}). The full + * sequence: {@link deployToken} → {@link deployLockbox} → {@link deployTokenPool} (passing the + * lockbox) → `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 @@ -170,7 +224,7 @@ export class EVMTokenManager extends TokenManager { * localTokenDecimals: 18, * rmnProxy: '0xRmnProxy...', * router: '0xRouter...', - * lockBox: '0xLockBox...', // required for LockReleaseTokenPool; must be a non-zero address + * lockbox: '0xLockbox...', // required for LockReleaseTokenPool; must be a non-zero address * wallet, * }) * ``` @@ -180,51 +234,44 @@ export class EVMTokenManager extends TokenManager { } /** - * 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 }`. - * @remarks Same post-deploy roles caveat as {@link deployToken} — the pool needs - * `grantMintAndBurnRoles` before it can bridge. + * 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 }`. + * @remarks Deploy the lockbox before its pool, then authorize the pool on it with + * `authorizeLockboxCallers` before the pool can lock/release. * @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 + * const unsigned = await cct.generateUnsignedDeployLockbox({ + * token: '0xToken...', // must be non-zero; the same token the LockReleaseTokenPool manages * sender: '0xDeployer...', * }) * ``` */ - generateUnsignedDeployToken(opts: DeployTokenParams): Promise { - return this.#deployToken.generate(this.chain, opts) + generateUnsignedDeployLockbox(opts: DeployLockboxParams): Promise { + return this.#deployLockbox.generate(this.chain, opts) } /** - * Deploys a `CrossChainToken` (v2.0.0), signing + submitting with `opts.wallet`; resolves - * to the tx hash and the newly deployed token address. - * @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)`. + * Deploys an `ERC20LockBox` (v2.0.0), signing + submitting with `opts.wallet`; resolves to the + * tx hash and the newly deployed lockbox address. + * @remarks Step two of the lock/release flow: {@link deployToken} → {@link deployLockbox} → + * {@link deployTokenPool} (passing this lockbox) → `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 } = await cct.deployToken({ - * name: 'My Token', - * symbol: 'MTK', - * decimals: 18, - * maxSupply: 0n, - * owner: '0xOwner...', + * const { hash, contractAddress } = await cct.deployLockbox({ + * token: '0xToken...', * wallet, * }) * ``` */ - deployToken(opts: EVMExecuteParams): Promise { - return this.#deployToken.execute(this.chain, opts) + deployLockbox(opts: EVMExecuteParams): Promise { + return this.#deployLockbox.execute(this.chain, opts) } } @@ -235,5 +282,6 @@ export type { DeployTokenPoolParams, DeployableTokenPoolType, } from './token-pool/operations/deploy-token-pool.ts' +export type { DeployLockboxParams } from './lockbox/operations/deploy-lockbox.ts' export type { DeployResult, EVMExecuteParams } from './operation.ts' export type { TransactionResult } from '../operation.ts' diff --git a/ccip-sdk/src/cct/evm/lockbox/interface.ts b/ccip-sdk/src/cct/evm/lockbox/interface.ts new file mode 100644 index 000000000..0f0176e35 --- /dev/null +++ b/ccip-sdk/src/cct/evm/lockbox/interface.ts @@ -0,0 +1,19 @@ +/** + * Deploy artifacts for `ERC20LockBox`: the cached {@link Interface} (constructor + calldata + * encoding) and the creation {@link LOCKBOX_BYTECODE}, built/loaded once from the vendored + * `artifacts/`. Only one lockbox version is deployable, so there is no version framework here — + * ops import these directly. + * + * @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' + +/** 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 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 000000000..b62c4f02d --- /dev/null +++ b/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.test.ts @@ -0,0 +1,148 @@ +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 }) + }) + + 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 000000000..4d8a835ff --- /dev/null +++ b/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts @@ -0,0 +1,70 @@ +/** + * 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 + * `authorizeLockboxCallers`. Mirrors `token-pool/operations/deploy-token-pool.ts`. + * + * @packageDocumentation + */ + +import { ZeroAddress } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import { + type DeployResult, + type EVMExecuteParams, + EVMOperation, + deploymentTx, +} from '../../operation.ts' +import { submit } from '../../submit.ts' +import { validateAddress } from '../../validate.ts' +import { LOCKBOX_BYTECODE, LOCKBOX_INTERFACE } from '../interface.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 }`. */ +export class DeployLockbox extends EVMOperation { + readonly name = 'deployLockbox' + + /** Validates the constructor params before building init-code. */ + protected validate(params: DeployLockboxParams): void { + validateAddress(this.name, 'token', params.token) + if (params.token === ZeroAddress) + throw new CCTParamsInvalidError(this.name, 'token', 'must not be the zero address') + } + + /** Builds a deployment tx (no `to`): creation bytecode + ABI-encoded constructor args. */ + protected buildUnsigned(_chain: EVMChain, params: DeployLockboxParams): UnsignedEVMTx { + return deploymentTx(LOCKBOX_BYTECODE, LOCKBOX_INTERFACE.encodeDeploy([params.token])) + } + + /** + * {@link generate}, then sign and submit; resolves to the tx hash and the newly deployed + * lockbox address (read from the mined receipt). + * @throws {@link CCTTxFailedError} if the tx mined without producing a contract address + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const { response, receipt } = await submit( + chain, + params.wallet, + await this.generate(chain, params), + 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 } + } +} 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 index 38a9f9bce..673e0c915 100644 --- 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 @@ -18,7 +18,7 @@ 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 LOCK_BOX = '0x' + '66'.repeat(20) +const LOCKBOX = '0x' + '66'.repeat(20) const DEPLOYED = '0x' + '77'.repeat(20) const HASH = '0x' + 'ab'.repeat(32) @@ -35,7 +35,7 @@ const W_LOCKBOX = '0000000000000000000000006666666666666666666666666666666666666 // 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`. +// 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 @@ -69,7 +69,7 @@ const CASES: { ...COMMON, type: 'LockReleaseTokenPool', advancedPoolHooks: HOOKS, - lockBox: LOCK_BOX, + lockbox: LOCKBOX, }, bytecode: LOCK_RELEASE_V2_0_0, ctorArgs: LOCK_RELEASE_ARGS, @@ -203,32 +203,32 @@ describe('DeployTokenPool (cct/evm token-pool operation)', () => { ) }) - it('rejects the zero address for a LockRelease lockBox', async () => { + it('rejects the zero address for a LockRelease lockbox', async () => { await assert.rejects( () => new DeployTokenPool().generate(stubChain(), { ...COMMON, type: 'LockReleaseTokenPool', advancedPoolHooks: HOOKS, - lockBox: ZeroAddress, + lockbox: ZeroAddress, }), - (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'lockBox', + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'lockbox', ) }) - it('rejects an invalid lockBox address', async () => { + it('rejects an invalid lockbox address', async () => { await assert.rejects( () => new DeployTokenPool().generate(stubChain(), { ...COMMON, type: 'LockReleaseTokenPool', advancedPoolHooks: HOOKS, - lockBox: 'nope', + lockbox: 'nope', }), - (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'lockBox', + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'lockbox', ) }) - // `lockBox` on a burn pool is a compile-time error (the DeployTokenPoolParams union), so + // `lockbox` on a burn pool is a compile-time error (the DeployTokenPoolParams union), so // there's no runtime case to test. }) 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 index 3e16b2c47..06c18248c 100644 --- 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 @@ -48,7 +48,7 @@ const TOKEN_POOL_BYTECODE = { export type DeployableTokenPoolType = keyof typeof TOKEN_POOL_BYTECODE /** Fields shared by every deployable token pool. */ -interface DeployTokenPoolBase { +interface DeployTokenPoolBaseParams { /** Address of the token the pool manages. */ token: string /** The token's `decimals` (uint8). */ @@ -64,28 +64,26 @@ interface DeployTokenPoolBase { } /** Params for a burn-* mint pool — the burn family shares one constructor shape. */ -export interface DeployBurnMintTokenPoolParams extends DeployTokenPoolBase { +export interface DeployBurnMintTokenPoolParams extends DeployTokenPoolBaseParams { type: Exclude } /** - * Params for a `LockReleaseTokenPool` — the burn constructor plus `lockBox`. + * Params for a `LockReleaseTokenPool` — the burn constructor plus `lockbox`. * - * @remarks Partial support: `lockBox` must be an already-deployed `ERC20LockBox` for the *same* - * `token` (the pool constructor calls `lockBox.isTokenSupported(token)` and reverts otherwise). - * This SDK does not yet deploy the lockbox or authorize the pool on it — deploy the `ERC20LockBox` - * and add the pool via the lockbox's `applyAuthorizedCallerUpdates` out-of-band before the pool can - * lock/release. A `deployLockBox` op + caller-authorization are tracked as a follow-up. + * @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 DeployTokenPoolBase { +export interface DeployLockReleaseTokenPoolParams extends DeployTokenPoolBaseParams { type: 'LockReleaseTokenPool' - /** Lock-box address; required and must be non-zero — the v2.0.0 constructor reverts on the zero address. */ - lockBox: string + /** 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). + * constructor; `LockReleaseTokenPool` additionally requires `lockbox` (a compile-time guarantee). */ export type DeployTokenPoolParams = DeployBurnMintTokenPoolParams | DeployLockReleaseTokenPoolParams @@ -102,7 +100,7 @@ const encodeBurnMintTokenPool: TokenPoolConstructorEncoder = (iface, p) => p.router, ]) -/** LockRelease constructor: the burn-* args plus `lockBox` (only that variant carries it). */ +/** LockRelease constructor: the burn-* args plus `lockbox` (only that variant carries it). */ const encodeLockReleaseTokenPool: TokenPoolConstructorEncoder = (iface, p) => iface.encodeDeploy([ p.token, @@ -110,7 +108,7 @@ const encodeLockReleaseTokenPool: TokenPoolConstructorEncoder = (iface, p) => p.advancedPoolHooks ?? ZeroAddress, p.rmnProxy, p.router, - p.type === 'LockReleaseTokenPool' ? p.lockBox : ZeroAddress, + p.type === 'LockReleaseTokenPool' ? p.lockbox : ZeroAddress, ]) /** Deploys a token pool; `execute` resolves to `{ hash, contractAddress }`. */ @@ -138,9 +136,9 @@ export class DeployTokenPool extends EVMOperation { if (params.advancedPoolHooks !== undefined) validateAddress(this.name, 'advancedPoolHooks', params.advancedPoolHooks) if (params.type === 'LockReleaseTokenPool') { - validateAddress(this.name, 'lockBox', params.lockBox) - if (params.lockBox === ZeroAddress) - throw new CCTParamsInvalidError(this.name, 'lockBox', 'must not be the zero address') + validateAddress(this.name, 'lockbox', params.lockbox) + if (params.lockbox === ZeroAddress) + throw new CCTParamsInvalidError(this.name, 'lockbox', 'must not be the zero address') } } From 229c73e7b50dcd5a75a6771a7c15b3c69164b2b3 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:12:33 +0100 Subject: [PATCH 43/87] feat(cct-sdk): Authorize Lockbox Callers (#312) * Better separation of concerns and abstractions * CCT: Add version dispatch + Transfer Ownership * Adjust with base * Address generic error types and doc examples * Fix linting * feat(cct-sdk): Add deploy evm token * Address PR comments * Address PR comments by fixing chain-specific types * feat(cct-sdk): Source chainlink/contracts-ccip artifacts (#303) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * Remove outdated bytecodes * feat(cct-sdk): Add CrossChainToken deployment + token versioning (#305) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * feat(cct-sdk): Add versioned Deploy Token (CCT + ERC20) * Remove outdated bytecodes * Deploy only CrossChainToken * Recover previous type changes * Address preMint specs * feat(cct-sdk): Add deploy evm token pool + type/version resolution * add remarks ts doc comment * Address PR comments * add lockfile to fix ci issue * Add comments * feat(cct-sdk): Add EVM deployLockbox operation (DAPP-10738) Vendor ERC20LockBox v2.0.0 artifacts; add the cached lockbox Interface + DeployLockbox op, wire generateUnsignedDeployLockbox/deployLockbox into EVMTokenManager, and document the LockRelease deploy sequence. * feat(cct-sdk): Add EVM authorizeLockboxCallers operation (DAPP-10788) Add AuthorizeLockboxCallers op (wraps ERC20LockBox applyAuthorizedCallerUpdates) + tests, and wire generateUnsignedAuthorizeLockboxCallers/authorizeLockboxCallers into EVMTokenManager to complete the LockRelease flow. * Update doc --- ccip-sdk/src/cct/evm/index.ts | 60 ++++- .../operations/authorize-callers.test.ts | 238 ++++++++++++++++++ .../lockbox/operations/authorize-callers.ts | 77 ++++++ .../evm/lockbox/operations/deploy-lockbox.ts | 2 +- 4 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.test.ts create mode 100644 ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 963e9a4a2..81458fe23 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -14,6 +14,10 @@ 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' @@ -35,6 +39,7 @@ export class EVMTokenManager extends TokenManager { readonly #deployToken = new DeployToken() readonly #deployTokenPool = new DeployTokenPool() readonly #deployLockbox = new DeployLockbox() + readonly #authorizeLockboxCallers = new AuthorizeLockboxCallers() /** Wraps an {@link EVMChain}; prefer the static factory methods. */ constructor(chain: EVMChain) { @@ -183,7 +188,7 @@ export class EVMTokenManager extends TokenManager { * 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) → `authorizeLockboxCallers` + * → {@link deployTokenPool} (passing the lockbox) → {@link authorizeLockboxCallers} * (`addedCallers: [pool]`) → {@link setPool} → configure lanes. * @throws {@link CCTParamsInvalidError} if any param is invalid * @example @@ -211,7 +216,7 @@ export class EVMTokenManager extends TokenManager { * 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) → `authorizeLockboxCallers` (`addedCallers: [pool]`) → {@link setPool} → + * 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 @@ -239,7 +244,7 @@ export class EVMTokenManager extends TokenManager { * only known once mined, so it is NOT returned here — use {@link deployLockbox} to receive * `{ hash, contractAddress }`. * @remarks Deploy the lockbox before its pool, then authorize the pool on it with - * `authorizeLockboxCallers` before the pool can lock/release. + * {@link authorizeLockboxCallers} before the pool can lock/release. * @throws {@link CCTParamsInvalidError} if any param is invalid * @example * ```typescript @@ -257,7 +262,7 @@ export class EVMTokenManager extends TokenManager { * Deploys an `ERC20LockBox` (v2.0.0), signing + submitting with `opts.wallet`; resolves to the * tx hash and the newly deployed lockbox address. * @remarks Step two of the lock/release flow: {@link deployToken} → {@link deployLockbox} → - * {@link deployTokenPool} (passing this lockbox) → `authorizeLockboxCallers` + * {@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 @@ -273,6 +278,52 @@ export class EVMTokenManager extends TokenManager { 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 — until + * then its lock/release reverts `UnauthorizedCaller(pool)`. + * @remarks Depositing the lockbox's initial liquidity is a manual final step with no SDK op: the + * depositor must itself be an authorized caller and have ERC20-approved the lockbox. + * @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) + } } export * from '../errors.ts' @@ -283,5 +334,6 @@ export type { DeployableTokenPoolType, } from './token-pool/operations/deploy-token-pool.ts' export type { DeployLockboxParams } from './lockbox/operations/deploy-lockbox.ts' +export type { AuthorizeLockboxCallersParams } from './lockbox/operations/authorize-callers.ts' export type { DeployResult, EVMExecuteParams } from './operation.ts' export type { TransactionResult } from '../operation.ts' 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 000000000..2b3d1734b --- /dev/null +++ b/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.test.ts @@ -0,0 +1,238 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { ZeroAddress, 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 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 000000000..7696f5b26 --- /dev/null +++ b/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts @@ -0,0 +1,77 @@ +/** + * 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 { ZeroAddress } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { EVMOperation } from '../../operation.ts' +import { validateAddress } from '../../validate.ts' +import { LOCKBOX_INTERFACE } from '../interface.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. + * + * Two validations here are SDK-side strictness, not contract behaviour: on-chain only *adds* revert + * `ZeroAddressNotAllowed`, and an update with both arrays empty is a successful owner-only no-op. + */ +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 { + validateAddress(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 => { + validateAddress(this.name, `${field}[${i}]`, c) + if (c === ZeroAddress) { + throw new CCTParamsInvalidError(this.name, `${field}[${i}]`, 'must not be the zero address') + } + } + 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 { family: ChainFamily.EVM, transactions: [{ to: lockbox, data }] } + } +} diff --git a/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts b/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts index 4d8a835ff..cbfff1891 100644 --- a/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts +++ b/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts @@ -2,7 +2,7 @@ * 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 - * `authorizeLockboxCallers`. Mirrors `token-pool/operations/deploy-token-pool.ts`. + * {@link AuthorizeLockboxCallers}. Mirrors `token-pool/operations/deploy-token-pool.ts`. * * @packageDocumentation */ From 8c8adf95342bb345c3e1be1122993756730105d0 Mon Sep 17 00:00:00 2001 From: Mervin Date: Thu, 30 Jul 2026 12:48:09 +0800 Subject: [PATCH 44/87] feat(cct-sdk): Add register token solana op (#310) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * fix: get pool state tests * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 96 ++++++- .../operations/append-to-lookup-table.ts | 9 +- .../token-admin-registry/operations/index.ts | 1 + .../operations/register-admin.test.ts | 194 ++++++++++++++ .../operations/register-admin.ts | 240 ++++++++++++++++++ 6 files changed, 533 insertions(+), 9 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/register-admin.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/register-admin.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 45fd48d2f..cbe9cb73b 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -31,6 +31,8 @@ describe('SolanaTokenManager (cct/solana)', () => { 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.getTokenPoolState, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index d459825f6..d5a98317e 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -28,16 +28,21 @@ import { type ExecuteAppendToLookupTableResult, type ExecuteCreateLookupTableParams, type ExecuteCreateLookupTableResult, + type ExecuteRegisterAdminParams, + type ExecuteRegisterAdminResult, type ExecuteSetPoolParams, type ExecuteSetPoolResult, type GenerateAppendToLookupTableParams, type GenerateAppendToLookupTableResult, type GenerateCreateLookupTableParams, type GenerateCreateLookupTableResult, + type GenerateRegisterAdminParams, + type GenerateRegisterAdminResult, type GenerateSetPoolParams, type GenerateSetPoolResult, AppendToLookupTable, CreateLookupTable, + RegisterAdmin, SetPool, } from './token-admin-registry/operations/index.ts' import { @@ -65,6 +70,7 @@ export class SolanaTokenManager extends TokenManager // Token admin registry operations readonly #appendToLookupTable = new AppendToLookupTable() readonly #createLookupTable = new CreateLookupTable() + readonly #registerAdmin = new RegisterAdmin() readonly #setPool = new SetPool() // Token pool operations @@ -407,16 +413,86 @@ export class SolanaTokenManager extends TokenManager return this.#appendToLookupTable.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 unsigned Solana `setPool` instructions. * - * The `payer` pays transaction fees. `authority` defaults to `payer`; 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. + * 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 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) @@ -434,12 +510,19 @@ export class SolanaTokenManager extends TokenManager } /** - * Registers a token pool. The wallet must be the token admin authority. For a newly deployed - * canonical pool, create the pool signer's ATA before calling this operation. + * 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 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) @@ -480,6 +563,9 @@ export class SolanaTokenManager extends TokenManager /** * 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 unsigned = await cct.generateUnsignedSetPool({ ...params, payer }) 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 index 4e5983882..016f4a70c 100644 --- 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 @@ -13,7 +13,7 @@ import { } from '../../operation.ts' import { deriveCcipLookupTableAddresses } from '../../programs/alt.ts' import { submit } from '../../submit.ts' -import { validatePublicKey } from '../../validate.ts' +import { validateAuthorityMatchesWallet, validatePublicKey } from '../../validate.ts' const MAX_ALT_ADDRESSES = 256 const EXTEND_CHUNK_SIZE = 30 @@ -171,10 +171,11 @@ export class AppendToLookupTable extends SolanaOperation< this.validate(generateParams) const authority = params.authority ? new PublicKey(params.authority) : undefined - if (authority && !authority.equals(wallet.publicKey)) { - throw new CCTParamsInvalidError( + if (authority) { + validateAuthorityMatchesWallet( this.name, - 'authority', + authority, + wallet.publicKey, 'appendToLookupTable requires authority to be the executing wallet. Use generateUnsignedAppendToLookupTable for vault-owned ALTs and have the vault sign/execute it.', ) } 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 index a054baa3f..105f934d5 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -1,3 +1,4 @@ export * from './append-to-lookup-table.ts' export * from './create-lookup-table.ts' +export * from './register-admin.ts' export * from './set-pool.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 000000000..5ffdb70a2 --- /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 000000000..db0dc9db9 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/register-admin.ts @@ -0,0 +1,240 @@ +import { unpackMint } from '@solana/spl-token' +import { type TransactionInstruction, 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 { 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 { validateAuthorityMatchesWallet, validatePublicKey } 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 + +/** 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 { + readonly name = 'registerAdmin' + + /** Validates all caller-supplied parameters before RPC. */ + protected validate(params: GenerateRegisterAdminParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'address', params.address) + validatePublicKey(this.name, 'payer', params.payer) + if (params.administrator) validatePublicKey(this.name, 'administrator', params.administrator) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + if ( + params.registrationMethod !== undefined && + !Object.values(REGISTER_ADMIN_METHODS).includes(params.registrationMethod) + ) { + throw new CCTParamsInvalidError( + this.name, + 'registrationMethod', + 'must be owner or ccip-admin', + ) + } + } + + /** Builds an unsigned token registration instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateRegisterAdminParams, + ): Promise { + const routerAddress = await chain.getTokenAdminRegistryFor(opts.address) + const router = new PublicKey(routerAddress) + const tokenMint = new PublicKey(opts.tokenAddress) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + + const mintAccount = await resolveTokenMint(chain.connection, tokenMint) + const { mintAuthority } = unpackMint(tokenMint, mintAccount, mintAccount.owner) + const administrator = opts.administrator ? new PublicKey(opts.administrator) : mintAuthority + const method = opts.registrationMethod ?? REGISTER_ADMIN_METHODS.OWNER + 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, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const payer = wallet.publicKey.toBase58() + const generateParams: GenerateRegisterAdminParams = { ...rest, payer } + this.validate(generateParams) + + const authority = params.authority ? new PublicKey(params.authority) : undefined + if (authority) { + validateAuthorityMatchesWallet( + this.name, + authority, + wallet.publicKey, + 'registerAdmin requires authority to be the executing wallet. Use generateUnsignedRegisterAdmin for externally signed transactions.', + ) + } + + const tx = await this.buildUnsigned(chain, generateParams) + return submit(chain, wallet, tx, this.name, computeUnits) + } +} From 10ea705970d2e3b42d03b556756e8d36d6e89ebf Mon Sep 17 00:00:00 2001 From: Mervin Date: Thu, 30 Jul 2026 23:36:48 +0800 Subject: [PATCH 45/87] feat(cct-sdk): Add transfer admin solana op (#313) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments --- ccip-sdk/src/cct/solana/index.test.ts | 15 +- ccip-sdk/src/cct/solana/index.ts | 69 ++++++++ .../token-admin-registry/operations/index.ts | 1 + .../operations/transfer-admin.test.ts | 157 ++++++++++++++++++ .../operations/transfer-admin.ts | 129 ++++++++++++++ 5 files changed, 367 insertions(+), 4 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index cbe9cb73b..ab345af45 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -19,14 +19,13 @@ describe('SolanaTokenManager (cct/solana)', () => { 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') - assert.equal(typeof cct.generateUnsignedCreateTokenMultisig, 'function') - assert.equal(typeof cct.createTokenMultisig, 'function') - assert.equal(typeof cct.generateUnsignedDeployTokenPool, 'function') - assert.equal(typeof cct.deployTokenPool, 'function') + + // Token admin registry operations assert.equal(typeof cct.generateUnsignedCreateLookupTable, 'function') assert.equal(typeof cct.createLookupTable, 'function') assert.equal(typeof cct.generateUnsignedAppendToLookupTable, 'function') @@ -35,6 +34,14 @@ describe('SolanaTokenManager (cct/solana)', () => { 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') + + // Token pool operations + 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.getTokenPoolState, 'function') }) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index d5a98317e..157028831 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -32,6 +32,8 @@ import { type ExecuteRegisterAdminResult, type ExecuteSetPoolParams, type ExecuteSetPoolResult, + type ExecuteTransferAdminParams, + type ExecuteTransferAdminResult, type GenerateAppendToLookupTableParams, type GenerateAppendToLookupTableResult, type GenerateCreateLookupTableParams, @@ -40,10 +42,13 @@ import { type GenerateRegisterAdminResult, type GenerateSetPoolParams, type GenerateSetPoolResult, + type GenerateTransferAdminParams, + type GenerateTransferAdminResult, AppendToLookupTable, CreateLookupTable, RegisterAdmin, SetPool, + TransferAdmin, } from './token-admin-registry/operations/index.ts' import { type ExecuteCreateTokenMultisigParams, @@ -72,6 +77,7 @@ export class SolanaTokenManager extends TokenManager readonly #createLookupTable = new CreateLookupTable() readonly #registerAdmin = new RegisterAdmin() readonly #setPool = new SetPool() + readonly #transferAdmin = new TransferAdmin() // Token pool operations readonly #createTokenMultisig = new CreateTokenMultisig() @@ -538,6 +544,69 @@ export class SolanaTokenManager extends TokenManager 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 a Burn/Mint, Lock/Release, or custom token pool's state account. * Pass `poolProgramAddress` instead of `poolType` for a custom pool program. 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 index 105f934d5..4f8ca2e4c 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -2,3 +2,4 @@ export * from './append-to-lookup-table.ts' export * from './create-lookup-table.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/transfer-admin.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin.test.ts new file mode 100644 index 000000000..0c51355bb --- /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 000000000..b6c855de0 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/transfer-admin.ts @@ -0,0 +1,129 @@ +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 { validateAuthorityMatchesWallet, validatePublicKey } 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 + +/** 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 { + readonly name = 'transferAdmin' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateTransferAdminParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'address', params.address) + validatePublicKey(this.name, 'newAdmin', params.newAdmin) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + } + + /** Builds the unsigned instruction after confirming the caller is the current admin. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateTransferAdminParams, + ): Promise { + const tokenMint = new PublicKey(opts.tokenAddress) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + const newAdmin = new PublicKey(opts.newAdmin) + const router = new PublicKey(await chain.getTokenAdminRegistryFor(opts.address)) + 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, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const payer = wallet.publicKey.toBase58() + const generateParams: GenerateTransferAdminParams = { ...rest, payer } + this.validate(generateParams) + + if (params.authority) { + validateAuthorityMatchesWallet( + this.name, + new PublicKey(params.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, generateParams), + this.name, + computeUnits, + ) + } +} From 1030c86c7757f6f0c0526450c48e0558762b3a82 Mon Sep 17 00:00:00 2001 From: Mervin Date: Fri, 31 Jul 2026 18:59:51 +0800 Subject: [PATCH 46/87] feat(cct-sdk): Add accept admin solana op (#317) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * feat: add accept admin op solana * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments * fix: address comments * fix: address comments * fix: update err.context.reason test --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 70 ++++++++ .../operations/accept-admin.test.ts | 152 ++++++++++++++++++ .../operations/accept-admin.ts | 129 +++++++++++++++ .../token-admin-registry/operations/index.ts | 1 + 5 files changed, 354 insertions(+) create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index ab345af45..5cad1f36c 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -26,6 +26,8 @@ describe('SolanaTokenManager (cct/solana)', () => { 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') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 157028831..0616687d4 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -24,6 +24,8 @@ import { CreateTokenAccount, } from './token/operations/index.ts' import { + type ExecuteAcceptAdminParams, + type ExecuteAcceptAdminResult, type ExecuteAppendToLookupTableParams, type ExecuteAppendToLookupTableResult, type ExecuteCreateLookupTableParams, @@ -34,6 +36,8 @@ import { type ExecuteSetPoolResult, type ExecuteTransferAdminParams, type ExecuteTransferAdminResult, + type GenerateAcceptAdminParams, + type GenerateAcceptAdminResult, type GenerateAppendToLookupTableParams, type GenerateAppendToLookupTableResult, type GenerateCreateLookupTableParams, @@ -44,6 +48,7 @@ import { type GenerateSetPoolResult, type GenerateTransferAdminParams, type GenerateTransferAdminResult, + AcceptAdmin, AppendToLookupTable, CreateLookupTable, RegisterAdmin, @@ -73,6 +78,7 @@ export class SolanaTokenManager extends TokenManager readonly #createTokenAccount = new CreateTokenAccount() // Token admin registry operations + readonly #acceptAdmin = new AcceptAdmin() readonly #appendToLookupTable = new AppendToLookupTable() readonly #createLookupTable = new CreateLookupTable() readonly #registerAdmin = new RegisterAdmin() @@ -419,6 +425,68 @@ export class SolanaTokenManager extends TokenManager 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. * @@ -493,6 +561,7 @@ export class SolanaTokenManager extends TokenManager * pool signer's ATA before calling this operation. * * @see {@link generateUnsignedRegisterAdmin} + * @see {@link generateUnsignedAcceptAdmin} * @see {@link generateUnsignedDeployTokenPool} * @see {@link generateUnsignedCreateTokenAccount} * @@ -521,6 +590,7 @@ export class SolanaTokenManager extends TokenManager * create the pool signer's ATA before calling this operation. * * @see {@link registerAdmin} + * @see {@link acceptAdmin} * @see {@link deployTokenPool} * @see {@link createTokenAccount} * 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 000000000..d8266c221 --- /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 000000000..bc4d3a99f --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/accept-admin.ts @@ -0,0 +1,129 @@ +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 { validateAuthorityMatchesWallet, validatePublicKey } 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 +} + +/** 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 { + readonly name = 'acceptAdmin' + + /** Validates all public keys before any RPC. */ + protected validate(params: GenerateAcceptAdminParams): void { + validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validatePublicKey(this.name, 'address', params.address) + validatePublicKey(this.name, 'payer', params.payer) + if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + } + + /** Builds the unsigned instruction after confirming the caller is the pending admin. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: GenerateAcceptAdminParams, + ): Promise { + const tokenMint = new PublicKey(opts.tokenAddress) + const payer = new PublicKey(opts.payer) + const authority = new PublicKey(opts.authority ?? opts.payer) + const router = new PublicKey(await chain.getTokenAdminRegistryFor(opts.address)) + 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 } + this.validate(generateParams) + + if (params.authority) { + validateAuthorityMatchesWallet( + this.name, + new PublicKey(params.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, generateParams), + this.name, + computeUnits, + ) + } +} 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 index 4f8ca2e4c..dfcd66949 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -1,3 +1,4 @@ +export * from './accept-admin.ts' export * from './append-to-lookup-table.ts' export * from './create-lookup-table.ts' export * from './register-admin.ts' From 21d698d8192807eb7e7febd0ad2caee353bee706 Mon Sep 17 00:00:00 2001 From: Mervin Date: Sat, 1 Aug 2026 00:02:04 +0800 Subject: [PATCH 47/87] fix(cct-sdk): Refactor solana token-admin-registry ops (#309) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * fix: address comments * fix: refactor solana ops * fix: package-lock file * fix: package script * fix: remove cct exports and add script to pack package including cct * fix: address comments * fix: address comments * fix: add comment --- ccip-sdk/src/cct/solana/index.ts | 51 ++- .../src/cct/solana/programs/token-pool.ts | 22 ++ ccip-sdk/src/cct/solana/serialize.test.ts | 2 +- ccip-sdk/src/cct/solana/submit.test.ts | 2 +- .../operations/append-to-lookup-table.test.ts | 301 ++++++++++++------ .../operations/append-to-lookup-table.ts | 56 +++- .../operations/create-lookup-table.test.ts | 219 ++++++++----- .../operations/create-lookup-table.ts | 36 ++- .../operations/set-pool.test.ts | 112 +++++-- .../operations/get-token-pool-state.ts | 87 +++-- ccip-sdk/src/cct/solana/validate.test.ts | 43 ++- ccip-sdk/src/cct/solana/validate.ts | 28 +- 12 files changed, 648 insertions(+), 311 deletions(-) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 0616687d4..71ea5ba82 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -118,9 +118,10 @@ export class SolanaTokenManager extends TokenManager /** * 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) @@ -143,9 +144,12 @@ export class SolanaTokenManager extends TokenManager /** * 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) @@ -207,6 +211,7 @@ export class SolanaTokenManager extends TokenManager * @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 @@ -281,9 +286,13 @@ export class SolanaTokenManager extends TokenManager /** * Builds unsigned Solana pool lookup table instructions. * - * Defaults to create+extend. 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`. + * 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 @@ -306,6 +315,12 @@ export class SolanaTokenManager extends TokenManager * 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) @@ -333,6 +348,8 @@ export class SolanaTokenManager extends TokenManager * @see {@link generateUnsignedCreateTokenAccount} * @see {@link generateUnsignedSetPool} * + * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid. + * * @example * ```ts * const cct = SolanaTokenManager.fromChain(chain) @@ -363,6 +380,10 @@ export class SolanaTokenManager extends TokenManager * @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) @@ -380,8 +401,13 @@ export class SolanaTokenManager extends TokenManager /** * Builds unsigned Solana lookup table extend instructions. * - * Pass `tokenAddress` and `poolProgramAddress` to append the standard CCIP pool addresses; - * pass `additionalAddresses` to append manual addresses. `authority` defaults to `payer`. + * 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 @@ -405,7 +431,14 @@ export class SolanaTokenManager extends TokenManager /** * Extends a Solana lookup table. * - * Pass `tokenAddress` and `poolProgramAddress` to append the standard CCIP pool addresses; + * 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 @@ -687,6 +720,7 @@ export class SolanaTokenManager extends TokenManager * * @example * ```ts + * const cct = SolanaTokenManager.fromChain(chain) * const state = await cct.getTokenPoolState({ * poolType: 'burn-mint', * tokenAddress: mint, @@ -707,6 +741,7 @@ export class SolanaTokenManager extends TokenManager * * @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') diff --git a/ccip-sdk/src/cct/solana/programs/token-pool.ts b/ccip-sdk/src/cct/solana/programs/token-pool.ts index 86273f97d..a15b4299c 100644 --- a/ccip-sdk/src/cct/solana/programs/token-pool.ts +++ b/ccip-sdk/src/cct/solana/programs/token-pool.ts @@ -22,6 +22,28 @@ export const TOKEN_POOL_PROGRAMS = { /** 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 diff --git a/ccip-sdk/src/cct/solana/serialize.test.ts b/ccip-sdk/src/cct/solana/serialize.test.ts index 97d78cb77..75835b10a 100644 --- a/ccip-sdk/src/cct/solana/serialize.test.ts +++ b/ccip-sdk/src/cct/solana/serialize.test.ts @@ -22,7 +22,7 @@ const unsigned = { ], } -describe('cct/solana serialize', () => { +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') diff --git a/ccip-sdk/src/cct/solana/submit.test.ts b/ccip-sdk/src/cct/solana/submit.test.ts index 703e95155..d3c4e6ecf 100644 --- a/ccip-sdk/src/cct/solana/submit.test.ts +++ b/ccip-sdk/src/cct/solana/submit.test.ts @@ -8,7 +8,7 @@ import { createCCTSubmitError } from './submit.ts' const OP = 'setPool' -describe('cct/solana submit error mapping', () => { +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) 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 index e0f022590..39d83a7e7 100644 --- 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 @@ -9,6 +9,7 @@ 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() @@ -17,24 +18,38 @@ 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, } -function stubChain(addresses: PublicKey[] = [], authority = AUTHORITY): SolanaChain { +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 () => ({ - value: { - state: { - authority: new PublicKey(authority), - addresses, + getAddressLookupTable: async () => { + onGetLookupTable?.() + return { + value: { + state: { + authority: new PublicKey(authority), + addresses, + }, }, - }, - }), + } + }, }, getTokenPoolConfig: async () => ({ token: TOKEN, @@ -55,110 +70,204 @@ function generate(opts = {}, chain = stubChain()) { }) } -describe('Solana TokenAdminRegistry appendToLookupTable', () => { - 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(), - ) - }) +describe('AppendToLookupTable (cct/solana)', () => { + describe('generate', () => { + it('builds extend ALT instructions', async () => { + const unsigned = await generate() - 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.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 1) + assert.equal( + unsigned.instructions[0]!.programId.toBase58(), + AddressLookupTableProgram.programId.toBase58(), + ) + }) - assert.equal(unsigned.instructions.length, 2) - }) + it('chunks additional addresses into multiple extend instructions', async () => { + const additionalAddresses = Array.from({ length: 31 }, () => + Keypair.generate().publicKey.toBase58(), + ) + const unsigned = await generate({ additionalAddresses }) - it('appends derived CCIP addresses before manual addresses', async () => { - const unsigned = await generate({ tokenAddress: TOKEN, poolProgramAddress: POOL_PROGRAM }) + assert.equal(unsigned.instructions.length, 2) + }) - assert.equal(unsigned.instructions.length, 1) - }) + 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, + ), + ), + ) - 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), + assert.deepEqual( + appendedAddresses.map((address) => address.toBase58()), + [...ccipAddresses, manualAddress].map((address) => address.toBase58()), + ) }) - await assert.rejects( - () => - generate( - { tokenAddress: TOKEN, poolProgramAddress: POOL_PROGRAM }, - stubChain(ccipAddresses), + 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(), ), - (err: unknown) => - err instanceof CCTParamsInvalidError && - err.context.operation === 'appendToLookupTable' && - err.context.param === 'lookupTableAddress', - ) + ) + }) + + 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', + ) + }) }) - it('rejects signed append when authority is not the wallet', async () => { - await assert.rejects( - () => - SolanaTokenManager.fromChain(stubChain()).appendToLookupTable({ + 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, - wallet: WALLET, - authority: AUTHORITY, - additionalAddresses: [Keypair.generate().publicKey.toBase58()], - }), - (err: unknown) => - err instanceof CCTParamsInvalidError && - err.context.operation === 'appendToLookupTable' && - err.context.param === 'authority', - ) - }) + payer: PAYER, + tokenAddress: TOKEN, + poolType: 'burn-mint', + poolProgramAddress: POOL_PROGRAM, + } as never), + CCTParamsInvalidError, + ) - it('rejects authority mismatch', async () => { - await assert.rejects( - () => generate({}, stubChain([], Keypair.generate().publicKey.toBase58())), - (err: unknown) => - err instanceof CCTParamsInvalidError && - err.context.operation === 'appendToLookupTable' && - err.context.param === 'authority', - ) - }) + assert.equal(getLookupTableCalls, 0) + }) - it('rejects ALTs over 256 addresses', async () => { - const currentAddresses = Array.from({ length: 256 }, () => Keypair.generate().publicKey) + it('rejects an invalid pool program address', async () => { + let getLookupTableCalls = 0 - await assert.rejects( - () => generate({}, stubChain(currentAddresses)), - (err: unknown) => - err instanceof CCTParamsInvalidError && - err.context.operation === 'appendToLookupTable' && - err.context.param === 'additionalAddresses', - ) - }) + 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 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', + ) + }) }) - it('requires token and pool program together', async () => { - await assert.rejects( - () => generate({ tokenAddress: TOKEN, poolProgramAddress: undefined }), - (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 index 016f4a70c..e98b90bfe 100644 --- 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 @@ -12,21 +12,42 @@ import { SolanaOperation, } from '../../operation.ts' import { deriveCcipLookupTableAddresses } from '../../programs/alt.ts' +import type { PoolProgramRef } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' -import { validateAuthorityMatchesWallet, validatePublicKey } from '../../validate.ts' +import { + resolvePoolProgram, + validateAuthorityMatchesWallet, + validatePublicKey, +} from '../../validate.ts' const MAX_ALT_ADDRESSES = 256 const EXTEND_CHUNK_SIZE = 30 -/** Parameters shared by Solana TokenAdminRegistry `appendToLookupTable` generation and execution. */ +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 - tokenAddress?: string - poolProgramAddress?: string - additionalAddresses?: 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 @@ -52,21 +73,22 @@ export class AppendToLookupTable extends SolanaOperation< validatePublicKey(this.name, 'lookupTableAddress', params.lookupTableAddress) validatePublicKey(this.name, 'payer', params.payer) if (params.authority) validatePublicKey(this.name, 'authority', params.authority) - if (params.tokenAddress) validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) - if (params.poolProgramAddress) { - validatePublicKey(this.name, 'poolProgramAddress', params.poolProgramAddress) - } - for (const [i, address] of (params.additionalAddresses ?? []).entries()) { - validatePublicKey(this.name, `additionalAddresses[${i}]`, address) - } - if (Boolean(params.tokenAddress) !== Boolean(params.poolProgramAddress)) { + const hasPoolProgramAddress = params.poolProgramAddress !== undefined + const hasPoolProgram = params.poolType !== undefined || hasPoolProgramAddress + if (Boolean(params.tokenAddress) !== hasPoolProgram) { throw new CCTParamsInvalidError( this.name, 'tokenAddress', - 'tokenAddress and poolProgramAddress must be provided together', + 'tokenAddress and exactly one of poolType or poolProgramAddress must be provided together', ) } + if (params.tokenAddress) validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + if (hasPoolProgram) resolvePoolProgram(this.name, params) + for (const [i, address] of (params.additionalAddresses ?? []).entries()) { + validatePublicKey(this.name, `additionalAddresses[${i}]`, address) + } + if (!params.tokenAddress && !params.additionalAddresses?.length) { throw new CCTParamsInvalidError( this.name, @@ -84,6 +106,7 @@ export class AppendToLookupTable extends SolanaOperation< const payer = new PublicKey(opts.payer) const authority = new PublicKey(opts.authority ?? opts.payer) const lookupTableAddress = new PublicKey(opts.lookupTableAddress) + const poolProgram = opts.tokenAddress ? resolvePoolProgram(this.name, opts) : undefined const lookupTable = await chain.connection.getAddressLookupTable(lookupTableAddress) if (!lookupTable.value) { @@ -104,8 +127,7 @@ export class AppendToLookupTable extends SolanaOperation< const addresses = [...(opts.additionalAddresses ?? []).map((a) => new PublicKey(a))] - if (opts.tokenAddress && opts.poolProgramAddress) { - const poolProgram = new PublicKey(opts.poolProgramAddress) + if (opts.tokenAddress && poolProgram) { const tokenMint = new PublicKey(opts.tokenAddress) const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { lookupTableAddress, 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 index 50f92f5b7..e8231117c 100644 --- 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 @@ -2,12 +2,13 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' import { TOKEN_PROGRAM_ID } from '@solana/spl-token' -import { AddressLookupTableProgram, Keypair } from '@solana/web3.js' +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() @@ -20,11 +21,14 @@ const WALLET = { signTransaction: async (tx: T) => tx, } -function stubChain(): SolanaChain { +function stubChain(onGetSlot?: () => void): SolanaChain { return { logger: { debug() {}, info() {}, warn() {}, error() {} }, connection: { - getSlot: async () => 123, + getSlot: async () => { + onGetSlot?.() + return 123 + }, getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }), }, getTokenPoolConfig: async () => ({ @@ -45,104 +49,145 @@ function generate(opts = {}) { }) } -describe('Solana TokenAdminRegistry createLookupTable', () => { - 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, - ) - }) +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('builds create-only ALT instruction in createEmpty mode', async () => { - const unsigned = await SolanaTokenManager.fromChain( - stubChain(), - ).generateUnsignedCreateLookupTable({ - payer: PAYER, - authority: AUTHORITY, - mode: 'createEmpty', + 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(), + ), + ) }) - 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('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', + 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)) }) - 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('chunks additional addresses into multiple extend instructions', async () => { - const additionalAddresses = Array.from({ length: 21 }, () => - Keypair.generate().publicKey.toBase58(), - ) - const unsigned = await generate({ additionalAddresses }) + it('uses caller-provided authority', async () => { + const unsigned = await generate({ authority: AUTHORITY }) - assert.equal(unsigned.instructions.length, 3) + 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', + ) + }) }) - it('rejects signed create+extend when authority is not the wallet', async () => { - await assert.rejects( - () => - SolanaTokenManager.fromChain(stubChain()).createLookupTable({ + 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, - wallet: WALLET, - authority: AUTHORITY, - }), - (err: unknown) => - err instanceof CCTParamsInvalidError && - err.context.operation === 'createLookupTable' && - err.context.param === 'authority', - ) - }) - - it('uses caller-provided authority', async () => { - const unsigned = await generate({ authority: AUTHORITY }) + payer: PAYER, + } as never), + CCTParamsInvalidError, + ) - assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === AUTHORITY)) + assert.equal(getSlotCalls, 0) + }) }) - 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('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 index 6ebfbc455..2dd592ac0 100644 --- 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 @@ -15,8 +15,13 @@ import { buildCreateLookupTableInstruction, deriveCcipLookupTableAddresses, } from '../../programs/alt.ts' +import type { PoolProgramRef } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' -import { validateAuthorityMatchesWallet, validatePublicKey } from '../../validate.ts' +import { + resolvePoolProgram, + validateAuthorityMatchesWallet, + validatePublicKey, +} from '../../validate.ts' const MAX_ALT_ADDRESSES = 256 const EXTEND_CHUNK_SIZE = 30 @@ -25,15 +30,14 @@ 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 - poolProgramAddress: 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 @@ -62,14 +66,14 @@ export class CreateLookupTable extends SolanaOperation< > { readonly name = 'createLookupTable' - /** Validates all public keys before any RPC. */ + /** Validates params before `buildUnsigned()` performs any RPC. */ protected validate(params: GenerateCreateLookupTableParams): void { validatePublicKey(this.name, 'payer', params.payer) if (params.authority) validatePublicKey(this.name, 'authority', params.authority) if (params.mode === 'createEmpty') return validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) - validatePublicKey(this.name, 'poolProgramAddress', params.poolProgramAddress) + resolvePoolProgram(this.name, params) for (const [i, address] of (params.additionalAddresses ?? []).entries()) { validatePublicKey(this.name, `additionalAddresses[${i}]`, address) } @@ -83,13 +87,12 @@ export class CreateLookupTable extends SolanaOperation< const payer = new PublicKey(opts.payer) const authority = new PublicKey(opts.authority ?? opts.payer) - const { instruction: createIx, lookupTableAddress } = buildCreateLookupTableInstruction({ - authority, - payer, - recentSlot: await chain.connection.getSlot('finalized'), - }) - 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()}`, ) @@ -101,10 +104,17 @@ export class CreateLookupTable extends SolanaOperation< } } - const poolProgram = new PublicKey(opts.poolProgramAddress) + // Validate and parse the pool program before calling slot RPC below. + const poolProgram = resolvePoolProgram(this.name, opts) const tokenMint = new PublicKey(opts.tokenAddress) const additionalAddresses = (opts.additionalAddresses ?? []).map((a) => new PublicKey(a)) + const { instruction: createIx, lookupTableAddress } = buildCreateLookupTableInstruction({ + authority, + payer, + recentSlot: await chain.connection.getSlot('finalized'), + }) + const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { lookupTableAddress, tokenMint, 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 index df1412f9a..8a44bea77 100644 --- 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 @@ -3,8 +3,10 @@ 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() @@ -39,48 +41,88 @@ function generate(opts = {}) { }) } -describe('Solana TokenAdminRegistry setPool', () => { - 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)) - }) +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] }) + 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') - }) + 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, + }) - 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) }) - 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)) + }) }) - it('uses caller-provided authority', async () => { - const unsigned = await generate({ authority: AUTHORITY }) + describe('validation', () => { + it('rejects invalid writable indexes before resolving the router', async () => { + let routerLookups = 0 - assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === AUTHORITY)) + 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-pool/operations/get-token-pool-state.ts b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.ts index 73d1e65ae..8d9b7b970 100644 --- 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 @@ -1,38 +1,20 @@ -import type { PublicKey } from '@solana/web3.js' - import { CCIPTokenPoolStateNotFoundError } from '../../../../errors/index.ts' import type { SolanaChain } from '../../../../solana/index.ts' -import { CCTParamsInvalidError } from '../../../errors.ts' import { + type PoolProgramRef, type TokenPoolConfig, decodeTokenPoolState, deriveTokenPoolConfigPda, - resolveTokenPoolProgram, } from '../../programs/token-pool.ts' import { SolanaQuery } from '../../query.ts' -import { parsePublicKey, validatePoolType } from '../../validate.ts' - -/** 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 -} +import { parsePublicKey, resolvePoolProgram } from '../../validate.ts' -/** 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 +export type { + BurnMintPoolProgramRef, + CustomPoolProgramRef, + LockReleasePoolProgramRef, + PoolProgramRef, +} from '../../programs/token-pool.ts' /** Parameters for reading a Solana token pool state. */ export type GetTokenPoolStateParams = PoolProgramRef & { @@ -57,6 +39,7 @@ type BaseConfig = { type GetTokenPoolStateResultBase = { stateAddress: string + /** Resolved pool program address: canonical for `poolType`, supplied for `poolProgramAddress`. */ programId: string version: number } @@ -74,30 +57,24 @@ export type LockReleaseGetTokenPoolStateResult = GetTokenPoolStateResultBase & { } } -/** State returned for a canonical or custom token pool program. */ -export type GetTokenPoolStateResult

= - P extends LockReleasePoolProgramRef - ? LockReleaseGetTokenPoolStateResult - : BaseGetTokenPoolStateResult - -function resolvePoolProgram(params: PoolProgramRef): PublicKey { - const hasPoolType = Object.hasOwn(params, 'poolType') - const hasPoolProgramAddress = Object.hasOwn(params, 'poolProgramAddress') - if (hasPoolType === hasPoolProgramAddress) { - throw new CCTParamsInvalidError( - 'getTokenPoolState', - 'poolType', - 'provide exactly one of poolType or poolProgramAddress', - ) - } - - if (hasPoolType) { - validatePoolType('getTokenPoolState', 'poolType', params.poolType) - return resolveTokenPoolProgram(params.poolType) - } +type TokenPoolStateResultByType = { + 'burn-mint': BaseGetTokenPoolStateResult + 'lock-release': LockReleaseGetTokenPoolStateResult +} - return parsePublicKey('getTokenPoolState', 'poolProgramAddress', params.poolProgramAddress) +/** + * State returned for a canonical or custom token pool program. + * + * Results queried with `poolProgramAddress` use the base config shape and omit lock-release-only + * fields, even when the supplied address is the lock-release program. + */ +export type GetTokenPoolStateResult

= P extends { + poolType: infer T } + ? T extends keyof TokenPoolStateResultByType + ? TokenPoolStateResultByType[T] + : BaseGetTokenPoolStateResult + : BaseGetTokenPoolStateResult function serializeBaseConfig(config: TokenPoolConfig): BaseConfig { return { @@ -127,8 +104,16 @@ export class GetTokenPoolState extends SolanaQuery< chain: SolanaChain, params: P, ): Promise> { + return this.fetchPoolState(chain, params) as Promise> + } + + /** Fetches and serializes the token pool configuration account. */ + private async fetchPoolState( + chain: SolanaChain, + params: GetTokenPoolStateParams, + ): Promise { const mint = parsePublicKey('getTokenPoolState', 'tokenAddress', params.tokenAddress) - const programId = resolvePoolProgram(params) + const programId = resolvePoolProgram('getTokenPoolState', params) const state = deriveTokenPoolConfigPda(programId, mint) const account = await chain.connection.getAccountInfo(state) @@ -161,9 +146,9 @@ export class GetTokenPoolState extends SolanaQuery< rebalancer: config.rebalancer.toBase58(), canAcceptLiquidity: config.canAcceptLiquidity, }, - } as GetTokenPoolStateResult

+ } } - return { ...result, config: baseConfig } as GetTokenPoolStateResult

+ return { ...result, config: baseConfig } } } diff --git a/ccip-sdk/src/cct/solana/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts index a7305f21b..ad0a2c9ec 100644 --- a/ccip-sdk/src/cct/solana/validate.test.ts +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -5,6 +5,7 @@ import { PublicKey } from '@solana/web3.js' import { parsePublicKey, + resolvePoolProgram, validateInteger, validateNonEmptyString, validatePoolType, @@ -13,8 +14,9 @@ import { validateWritableIndexes, } from './validate.ts' import { CCTParamsInvalidError } from '../errors.ts' +import { type PoolProgramRef, TOKEN_POOL_PROGRAMS } from './programs/token-pool.ts' -describe('cct/solana validate', () => { +describe('Validate (cct/solana)', () => { it('parses valid public keys', () => { const key = parsePublicKey('op', 'payer', PublicKey.default.toBase58()) assert.ok(key.equals(PublicKey.default)) @@ -74,6 +76,45 @@ describe('cct/solana validate', () => { ) }) + 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)) diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index fdc33255b..c9f47b278 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -3,7 +3,12 @@ import { PublicKey } from '@solana/web3.js' import { CCIPAddressInvalidError } from '../../errors/index.ts' import { ChainFamily } from '../../networks.ts' import { CCTParamsInvalidError } from '../errors.ts' -import { type TokenPoolType, TOKEN_POOL_PROGRAMS } from './programs/token-pool.ts' +import { + type PoolProgramRef, + type TokenPoolType, + TOKEN_POOL_PROGRAMS, + resolveTokenPoolProgram, +} from './programs/token-pool.ts' /** * Parses `value` as a Solana public key. @@ -91,6 +96,27 @@ export function validatePoolType( } } +/** 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. From bba984bc8f2b013e1a6882a8ac8f184623798e89 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:05:35 +0100 Subject: [PATCH 48/87] feat(cct-sdk): Add deploy verification + unify ops (#316) * Better separation of concerns and abstractions * CCT: Add version dispatch + Transfer Ownership * Adjust with base * Address generic error types and doc examples * Fix linting * feat(cct-sdk): Add deploy evm token * Address PR comments * Address PR comments by fixing chain-specific types * feat(cct-sdk): Source chainlink/contracts-ccip artifacts (#303) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * Remove outdated bytecodes * feat(cct-sdk): Add CrossChainToken deployment + token versioning (#305) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * feat(cct-sdk): Add versioned Deploy Token (CCT + ERC20) * Remove outdated bytecodes * Deploy only CrossChainToken * Recover previous type changes * Address preMint specs * feat(cct-sdk): Add deploy evm token pool + type/version resolution * add remarks ts doc comment * Address PR comments * add lockfile to fix ci issue * Add comments * feat(cct-sdk): Add EVM deployLockbox operation (DAPP-10738) Vendor ERC20LockBox v2.0.0 artifacts; add the cached lockbox Interface + DeployLockbox op, wire generateUnsignedDeployLockbox/deployLockbox into EVMTokenManager, and document the LockRelease deploy sequence. * feat(cct-sdk): Add EVM authorizeLockboxCallers operation (DAPP-10788) Add AuthorizeLockboxCallers op (wraps ERC20LockBox applyAuthorizedCallerUpdates) + tests, and wire generateUnsignedAuthorizeLockboxCallers/authorizeLockboxCallers into EVMTokenManager to complete the LockRelease flow. * refactor(cct-sdk): add validateNonZeroAddress + guard single-tx submit * feat(cct-sdk): EVM deploy verification + unify deploy ops * Address PR comments * Review pass --- ccip-sdk/src/cct/evm/index.ts | 35 +++--- ccip-sdk/src/cct/evm/lockbox/contracts.ts | 29 +++++ ccip-sdk/src/cct/evm/lockbox/interface.ts | 19 ---- .../operations/authorize-callers.test.ts | 29 ++++- .../lockbox/operations/authorize-callers.ts | 24 ++-- .../lockbox/operations/deploy-lockbox.test.ts | 6 +- .../evm/lockbox/operations/deploy-lockbox.ts | 54 +++------ ccip-sdk/src/cct/evm/operation.ts | 106 ++++++++++++++++-- ccip-sdk/src/cct/evm/submit.ts | 5 +- .../operations/set-pool.ts | 5 +- .../{version.test.ts => contracts.test.ts} | 2 +- .../token-pool/{version.ts => contracts.ts} | 44 +++++++- .../operations/deploy-token-pool.test.ts | 20 +++- .../operations/deploy-token-pool.ts | 87 ++++---------- .../operations/transfer-ownership.ts | 11 +- ccip-sdk/src/cct/evm/token/contracts.ts | 69 ++++++++++++ .../evm/token/operations/deploy-token.test.ts | 15 ++- .../cct/evm/token/operations/deploy-token.ts | 49 ++------ ccip-sdk/src/cct/evm/token/version.ts | 74 ------------ ccip-sdk/src/cct/evm/validate.ts | 24 +++- 20 files changed, 406 insertions(+), 301 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/lockbox/contracts.ts delete mode 100644 ccip-sdk/src/cct/evm/lockbox/interface.ts rename ccip-sdk/src/cct/evm/token-pool/{version.test.ts => contracts.test.ts} (99%) rename ccip-sdk/src/cct/evm/token-pool/{version.ts => contracts.ts} (75%) create mode 100644 ccip-sdk/src/cct/evm/token/contracts.ts delete mode 100644 ccip-sdk/src/cct/evm/token/version.ts diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 81458fe23..a49a0820e 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -133,7 +133,7 @@ export class EVMTokenManager extends TokenManager { /** * 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 }`. + * 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 @@ -155,7 +155,8 @@ export class EVMTokenManager extends TokenManager { /** * Deploys a `CrossChainToken` (v2.0.0), signing + submitting with `opts.wallet`; resolves - * to the tx hash and the newly deployed token address. + * 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)`. @@ -164,7 +165,7 @@ export class EVMTokenManager extends TokenManager { * @throws {@link CCTTxFailedError} if the tx reverts, fails, or mines without an address * @example * ```typescript - * const { hash, contractAddress } = await cct.deployToken({ + * const { hash, contractAddress, verification } = await cct.deployToken({ * name: 'My Token', * symbol: 'MTK', * decimals: 18, @@ -183,7 +184,7 @@ export class EVMTokenManager extends TokenManager { * 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 }`. + * `{ 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}) @@ -208,8 +209,9 @@ export class EVMTokenManager extends TokenManager { } /** - * Deploys a token pool, signing + submitting with `opts.wallet`; resolves to the tx hash - * and the newly deployed pool address. `type` selects the pool contract (a + * 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 @@ -223,7 +225,7 @@ export class EVMTokenManager extends TokenManager { * @throws {@link CCTTxFailedError} if the tx reverts, fails, or mines without an address * @example * ```typescript - * const { hash, contractAddress } = await cct.deployTokenPool({ + * const { hash, contractAddress, verification } = await cct.deployTokenPool({ * type: 'LockReleaseTokenPool', * token: '0xToken...', * localTokenDecimals: 18, @@ -242,7 +244,7 @@ export class EVMTokenManager extends TokenManager { * 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 }`. + * `{ 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 @@ -260,7 +262,8 @@ export class EVMTokenManager extends TokenManager { /** * Deploys an `ERC20LockBox` (v2.0.0), signing + submitting with `opts.wallet`; resolves to the - * tx hash and the newly deployed lockbox address. + * 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. @@ -269,7 +272,7 @@ export class EVMTokenManager extends TokenManager { * @throws {@link CCTTxFailedError} if the tx reverts, fails, or mines without an address * @example * ```typescript - * const { hash, contractAddress } = await cct.deployLockbox({ + * const { hash, contractAddress, verification } = await cct.deployLockbox({ * token: '0xToken...', * wallet, * }) @@ -302,10 +305,7 @@ export class EVMTokenManager extends TokenManager { /** * Adds/removes authorized callers on an `ERC20LockBox`, signing + submitting with `opts.wallet` - * (the lockbox owner). Authorize the `LockReleaseTokenPool` before it can lock/release — until - * then its lock/release reverts `UnauthorizedCaller(pool)`. - * @remarks Depositing the lockbox's initial liquidity is a manual final step with no SDK op: the - * depositor must itself be an authorized caller and have ERC20-approved the lockbox. + * (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 @@ -335,5 +335,10 @@ export type { } from './token-pool/operations/deploy-token-pool.ts' export type { DeployLockboxParams } from './lockbox/operations/deploy-lockbox.ts' export type { AuthorizeLockboxCallersParams } from './lockbox/operations/authorize-callers.ts' -export type { DeployResult, EVMExecuteParams } from './operation.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 000000000..afbe70862 --- /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/interface.ts b/ccip-sdk/src/cct/evm/lockbox/interface.ts deleted file mode 100644 index 0f0176e35..000000000 --- a/ccip-sdk/src/cct/evm/lockbox/interface.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Deploy artifacts for `ERC20LockBox`: the cached {@link Interface} (constructor + calldata - * encoding) and the creation {@link LOCKBOX_BYTECODE}, built/loaded once from the vendored - * `artifacts/`. Only one lockbox version is deployable, so there is no version framework here — - * ops import these directly. - * - * @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' - -/** 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 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 index 2b3d1734b..72b7a5854 100644 --- a/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.test.ts +++ b/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' -import { ZeroAddress, makeError } from 'ethers' +import { ZeroAddress, getIcapAddress, makeError } from 'ethers' import { AuthorizeLockboxCallers } from './authorize-callers.ts' import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' @@ -129,6 +129,33 @@ describe('AuthorizeLockboxCallers (cct/evm lockbox operation)', () => { ) }) + 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 }), diff --git a/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts b/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts index 7696f5b26..e2650fc6c 100644 --- a/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts +++ b/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts @@ -6,24 +6,18 @@ * @packageDocumentation */ -import { ZeroAddress } from 'ethers' - import type { EVMChain } from '../../../../evm/index.ts' import type { UnsignedEVMTx } from '../../../../evm/types.ts' -import { ChainFamily } from '../../../../networks.ts' import { CCTParamsInvalidError } from '../../../errors.ts' -import { EVMOperation } from '../../operation.ts' -import { validateAddress } from '../../validate.ts' -import { LOCKBOX_INTERFACE } from '../interface.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. - * - * Two validations here are SDK-side strictness, not contract behaviour: on-chain only *adds* revert - * `ZeroAddressNotAllowed`, and an update with both arrays empty is a successful owner-only no-op. */ export interface AuthorizeLockboxCallersParams { /** Address of the `ERC20LockBox` to update. */ @@ -46,7 +40,7 @@ export class AuthorizeLockboxCallers extends EVMOperation { - validateAddress(this.name, `${field}[${i}]`, c) - if (c === ZeroAddress) { - throw new CCTParamsInvalidError(this.name, `${field}[${i}]`, 'must not be the zero address') - } - } + 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)) } @@ -72,6 +62,6 @@ export class AuthorizeLockboxCallers extends EVMOperation { token: TOKEN, wallet: fakeSigner({ contractAddress: DEPLOYED }), }) - assert.deepEqual(result, { hash: HASH, 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 () => { diff --git a/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts b/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts index cbfff1891..70309fe4d 100644 --- a/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts +++ b/ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts @@ -7,20 +7,11 @@ * @packageDocumentation */ -import { ZeroAddress } from 'ethers' +import type { Interface } from 'ethers' -import type { EVMChain } from '../../../../evm/index.ts' -import type { UnsignedEVMTx } from '../../../../evm/types.ts' -import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' -import { - type DeployResult, - type EVMExecuteParams, - EVMOperation, - deploymentTx, -} from '../../operation.ts' -import { submit } from '../../submit.ts' -import { validateAddress } from '../../validate.ts' -import { LOCKBOX_BYTECODE, LOCKBOX_INTERFACE } from '../interface.ts' +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 { @@ -30,41 +21,22 @@ export interface DeployLockboxParams { sender?: string } -/** Deploys an `ERC20LockBox`; `execute` resolves to `{ hash, contractAddress }`. */ -export class DeployLockbox extends EVMOperation { +/** 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 { - validateAddress(this.name, 'token', params.token) - if (params.token === ZeroAddress) - throw new CCTParamsInvalidError(this.name, 'token', 'must not be the zero address') + validateNonZeroAddress(this.name, 'token', params.token) } - /** Builds a deployment tx (no `to`): creation bytecode + ABI-encoded constructor args. */ - protected buildUnsigned(_chain: EVMChain, params: DeployLockboxParams): UnsignedEVMTx { - return deploymentTx(LOCKBOX_BYTECODE, LOCKBOX_INTERFACE.encodeDeploy([params.token])) + /** Deploy artifact for `ERC20LockBox` (v2.0.0). */ + protected artifact(): DeployArtifact { + return getLockboxArtifact() } - /** - * {@link generate}, then sign and submit; resolves to the tx hash and the newly deployed - * lockbox address (read from the mined receipt). - * @throws {@link CCTTxFailedError} if the tx mined without producing a contract address - */ - override async execute( - chain: EVMChain, - params: EVMExecuteParams, - ): Promise { - const { response, receipt } = await submit( - chain, - params.wallet, - await this.generate(chain, params), - 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 } + /** 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 index d0f294324..4dc0b157c 100644 --- a/ccip-sdk/src/cct/evm/operation.ts +++ b/ccip-sdk/src/cct/evm/operation.ts @@ -1,38 +1,86 @@ /** * EVM {@link Operation} lifecycle: validate → encode → submit. * Concrete ops implement {@link EVMOperation.buildUnsigned}; the base wires - * {@link generate} and {@link execute}. Ops needing more than a tx hash (e.g. a - * deployment's address) override {@link execute}, reusing {@link submit}. + * {@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 } from 'ethers' + import type { EVMChain } from '../../evm/index.ts' import type { UnsignedEVMTx } from '../../evm/types.ts' import { ChainFamily } from '../../networks.ts' +import { 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 deploymentTx(bytecode: `0x${string}`, ctorArgs: string): UnsignedEVMTx { +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.). No block-explorer verification handle - * yet; it's recoverable from the init-code, so adding one later is non-breaking. + * 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 } +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) override {@link execute}. + * resolve to more (e.g. a deployed address) extend {@link EVMDeployOperation}. */ export abstract class EVMOperation

extends Operation< EVMChain, @@ -66,3 +114,47 @@ export abstract class EVMOperation

extends Operat 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/submit.ts b/ccip-sdk/src/cct/evm/submit.ts index 7e0c8add1..45c5dc418 100644 --- a/ccip-sdk/src/cct/evm/submit.ts +++ b/ccip-sdk/src/cct/evm/submit.ts @@ -48,10 +48,13 @@ export async function submit( 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 = { ...unsigned.transactions[0]! } + 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) 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 index a31f5736d..ed48c3ab5 100644 --- 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 @@ -8,8 +8,7 @@ import { interfaces } from '../../../../evm/const.ts' import type { EVMChain } from '../../../../evm/index.ts' import type { UnsignedEVMTx } from '../../../../evm/types.ts' -import { ChainFamily } from '../../../../networks.ts' -import { EVMOperation } from '../../operation.ts' +import { EVMOperation, callTx } from '../../operation.ts' import { validateAddress } from '../../validate.ts' /** Parameters for `setPool`. Zero `poolAddress` delists the token. */ @@ -45,6 +44,6 @@ export class SetPool extends EVMOperation { p.tokenAddress, p.poolAddress, ]) - return { family: ChainFamily.EVM, transactions: [{ to, data }] } + return callTx(to, data) } } diff --git a/ccip-sdk/src/cct/evm/token-pool/version.test.ts b/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts similarity index 99% rename from ccip-sdk/src/cct/evm/token-pool/version.test.ts rename to ccip-sdk/src/cct/evm/token-pool/contracts.test.ts index 48ad8c854..011081726 100644 --- a/ccip-sdk/src/cct/evm/token-pool/version.test.ts +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts @@ -14,7 +14,7 @@ import { isTokenPoolVersion, parseTokenPoolVersion, resolveEncoder, -} from './version.ts' +} from './contracts.ts' import { CCTContractTypeInvalidError, CCTContractVersionUnsupportedError, diff --git a/ccip-sdk/src/cct/evm/token-pool/version.ts b/ccip-sdk/src/cct/evm/token-pool/contracts.ts similarity index 75% rename from ccip-sdk/src/cct/evm/token-pool/version.ts rename to ccip-sdk/src/cct/evm/token-pool/contracts.ts index 9ac182555..511c59bb5 100644 --- a/ccip-sdk/src/cct/evm/token-pool/version.ts +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.ts @@ -1,7 +1,8 @@ /** - * EVM token-pool version axis for CCT: resolve an on-chain pool's type + version - * ({@link resolveTokenPool}), select its cached ABI ({@link getTokenPoolInterface}), and - * floor-match version-keyed encoders ({@link resolveEncoder}). + * 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 */ @@ -22,6 +23,11 @@ import BURN_MINT_TOKEN_POOL_V1_6_1_ABI from '../artifacts/abi/V1_6_1/burn-mint-t 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 @@ -150,6 +156,38 @@ export function getTokenPoolInterface(type: TokenPoolType, version: TokenPoolVer 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. 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 index 673e0c915..21576292f 100644 --- 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 @@ -244,9 +244,27 @@ describe('DeployTokenPool (cct/evm token-pool operation)', () => { ...params, wallet: fakeSigner({ contractAddress: DEPLOYED }), }) - assert.deepEqual(result, { hash: HASH, 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( () => 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 index 06c18248c..d35b16c97 100644 --- 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 @@ -8,44 +8,19 @@ import { type Interface, ZeroAddress } from 'ethers' -import type { EVMChain } from '../../../../evm/index.ts' -import type { UnsignedEVMTx } from '../../../../evm/types.ts' -import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.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 DeployResult, - type EVMExecuteParams, - EVMOperation, - deploymentTx, -} from '../../operation.ts' -import { submit } from '../../submit.ts' -import { validateAddress, validateUint8 } from '../../validate.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { type DeployArtifact, EVMDeployOperation } from '../../operation.ts' +import { validateAddress, validateNonZeroAddress, validateUint8 } from '../../validate.ts' import { + type DeployableTokenPoolType, type TokenPoolFamily, - type TokenPoolType, - TokenPoolVersion, + getTokenPoolArtifact, getTokenPoolFamily, - getTokenPoolInterface, -} from '../version.ts' - -/** - * 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} derives from them). 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> + isDeployableTokenPoolType, +} from '../contracts.ts' -/** A pool contract type that can be deployed (has vendored 2.0.0 creation bytecode). */ -export type DeployableTokenPoolType = keyof typeof TOKEN_POOL_BYTECODE +/** Deployable pool types + their creation bytecode/artifact live in `../contracts.ts`. */ +export type { DeployableTokenPoolType } /** Fields shared by every deployable token pool. */ interface DeployTokenPoolBaseParams { @@ -111,8 +86,8 @@ const encodeLockReleaseTokenPool: TokenPoolConstructorEncoder = (iface, p) => p.type === 'LockReleaseTokenPool' ? p.lockbox : ZeroAddress, ]) -/** Deploys a token pool; `execute` resolves to `{ hash, contractAddress }`. */ -export class DeployTokenPool extends EVMOperation { +/** 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. */ @@ -123,7 +98,7 @@ export class DeployTokenPool extends EVMOperation { /** Validates the constructor params before building init-code. */ protected validate(params: DeployTokenPoolParams): void { - if (!Object.hasOwn(TOKEN_POOL_BYTECODE, params.type)) + if (!isDeployableTokenPoolType(params.type)) throw new CCTParamsInvalidError( this.name, 'type', @@ -135,39 +110,17 @@ export class DeployTokenPool extends EVMOperation { validateAddress(this.name, 'router', params.router) if (params.advancedPoolHooks !== undefined) validateAddress(this.name, 'advancedPoolHooks', params.advancedPoolHooks) - if (params.type === 'LockReleaseTokenPool') { - validateAddress(this.name, 'lockbox', params.lockbox) - if (params.lockbox === ZeroAddress) - throw new CCTParamsInvalidError(this.name, 'lockbox', 'must not be the zero address') - } + if (params.type === 'LockReleaseTokenPool') + validateNonZeroAddress(this.name, 'lockbox', params.lockbox) } - /** Builds a deployment tx (no `to`): creation bytecode + ABI-encoded constructor args. */ - protected buildUnsigned(_chain: EVMChain, params: DeployTokenPoolParams): UnsignedEVMTx { - const iface = getTokenPoolInterface(params.type, TokenPoolVersion.V2_0_0) - const encode = this.encoders[getTokenPoolFamily(params.type)] - return deploymentTx(TOKEN_POOL_BYTECODE[params.type], encode(iface, params)) + /** Deploy artifact for the selected pool `type` (v2.0.0): name + ctor interface + bytecode. */ + protected artifact(p: DeployTokenPoolParams): DeployArtifact { + return getTokenPoolArtifact(p.type) } - /** - * {@link generate}, then sign and submit; resolves to the tx hash and the newly deployed - * pool address (read from the mined receipt). - * @throws {@link CCTTxFailedError} if the tx mined without producing a contract address - */ - override async execute( - chain: EVMChain, - params: EVMExecuteParams, - ): Promise { - const { response, receipt } = await submit( - chain, - params.wallet, - await this.generate(chain, params), - 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 } + /** 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/transfer-ownership.ts b/ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts index acf2f6043..ae9ce1db0 100644 --- a/ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts +++ b/ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts @@ -9,15 +9,14 @@ import type { Interface } from 'ethers' import type { EVMChain } from '../../../../evm/index.ts' import type { UnsignedEVMTx } from '../../../../evm/types.ts' -import { ChainFamily } from '../../../../networks.ts' -import { EVMOperation } from '../../operation.ts' +import { EVMOperation, callTx } from '../../operation.ts' import { validateAddress } from '../../validate.ts' import { TokenPoolVersion, getTokenPoolInterface, resolveEncoder, resolveTokenPool, -} from '../version.ts' +} from '../contracts.ts' /** Parameters for {@link TransferOwnership}. */ export interface TransferOwnershipParams { @@ -30,10 +29,8 @@ export interface TransferOwnershipParams { /** Encodes `transferOwnership` calldata against the resolved pool {@link Interface}. */ type Encoder = (iface: Interface, params: TransferOwnershipParams) => UnsignedEVMTx -const encodeTransferOwnership: Encoder = (iface, { newOwner, poolAddress }) => { - const data = iface.encodeFunctionData('transferOwnership', [newOwner]) - return { family: ChainFamily.EVM, transactions: [{ to: poolAddress, data }] } -} +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 { 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 000000000..cdd3aa180 --- /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 index e8980ca56..f24f6072d 100644 --- a/ccip-sdk/src/cct/evm/token/operations/deploy-token.test.ts +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-token.test.ts @@ -218,7 +218,20 @@ describe('DeployToken (cct/evm)', () => { ...INPUTS, wallet: fakeSigner({ contractAddress: DEPLOYED }), }) - assert.deepEqual(result, { hash: HASH, 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 () => { diff --git a/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts b/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts index dbb0fe5ed..69e2c3d5a 100644 --- a/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts @@ -7,23 +7,15 @@ import { type Interface, ZeroAddress } from 'ethers' -import type { EVMChain } from '../../../../evm/index.ts' -import type { UnsignedEVMTx } from '../../../../evm/types.ts' -import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' -import { - type DeployResult, - type EVMExecuteParams, - EVMOperation, - deploymentTx, -} from '../../operation.ts' -import { submit } from '../../submit.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { type DeployArtifact, EVMDeployOperation } from '../../operation.ts' import { validateAddress, validateNonEmptyString, validateUint256, validateUint8, } from '../../validate.ts' -import { TokenVersion, tokenArtifact } from '../version.ts' +import { TokenVersion, getTokenArtifact } from '../contracts.ts' /** Parameters for {@link DeployToken} — deploys `CrossChainToken` (v2.0.0). */ export interface DeployTokenParams { @@ -63,8 +55,8 @@ function encodeCrossChainToken(iface: Interface, p: DeployTokenParams): string { ]) } -/** Deploys a `CrossChainToken`; `execute` resolves to `{ hash, contractAddress }`. */ -export class DeployToken extends EVMOperation { +/** 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. */ @@ -109,32 +101,13 @@ export class DeployToken extends EVMOperation { validateAddress(this.name, 'burnMintRoleAdmin', params.burnMintRoleAdmin) } - /** Builds a deployment tx (no `to`): creation bytecode + ABI-encoded constructor args. */ - protected buildUnsigned(_chain: EVMChain, params: DeployTokenParams): UnsignedEVMTx { - // hardcoded to deploy CrossChainToken 2.0.0 - const { iface, bytecode } = tokenArtifact(TokenVersion.V2_0_0) - return deploymentTx(bytecode, encodeCrossChainToken(iface, params)) + /** Deploy artifact for `CrossChainToken` (v2.0.0). */ + protected artifact(): DeployArtifact { + return getTokenArtifact(TokenVersion.V2_0_0) } - /** - * {@link generate}, then sign and submit; resolves to the tx hash and the newly - * deployed contract address (read from the mined receipt). - * @throws {@link CCTTxFailedError} if the tx mined without producing a contract address - */ - override async execute( - chain: EVMChain, - params: EVMExecuteParams, - ): Promise { - const { response, receipt } = await submit( - chain, - params.wallet, - await this.generate(chain, params), - 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 } + /** 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/token/version.ts b/ccip-sdk/src/cct/evm/token/version.ts deleted file mode 100644 index ae164bb0d..000000000 --- a/ccip-sdk/src/cct/evm/token/version.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * EVM token version axis for CCT. {@link TokenVersion} + {@link TOKEN_ABIS} cover every - * known token contract so read/write ops can resolve the right interface; - * {@link TOKEN_ARTIFACTS} / {@link tokenArtifact} add creation bytecode. `2.0.0` is - * `CrossChainToken`; `1.5.1` / `1.6.2` are `FactoryBurnMintERC20`. Mirrors - * `token-pool/version.ts`. - * - * @packageDocumentation - */ - -import { type InterfaceAbi, 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' - -/** - * 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] - -/** Contract ABI per {@link TokenVersion} — lets read/write ops resolve the right interface. */ -export const TOKEN_ABIS: Record = { - [TokenVersion.V1_5_1]: FACTORY_BURN_MINT_ERC20_V1_5_1_ABI, - [TokenVersion.V1_6_2]: FACTORY_BURN_MINT_ERC20_V1_6_2_ABI, - [TokenVersion.V2_0_0]: CROSS_CHAIN_TOKEN_V2_0_0_ABI, -} - -/** - * Returns the contract ABI for `version`. - * @throws {@link CCTContractVersionUnsupportedError} if `version` has no vendored ABI - */ -export function tokenAbi(version: TokenVersion): InterfaceAbi { - const abi = TOKEN_ABIS[version] - if (!abi) throw new CCTContractVersionUnsupportedError('token', version) - return abi -} - -/** A token deploy artifact: the cached constructor {@link Interface} and creation bytecode. */ -export interface TokenArtifact { - iface: Interface - bytecode: `0x${string}` -} - -/** - * Deploy artifacts (ctor {@link Interface} + creation bytecode) keyed by {@link TokenVersion}, - * built once. Only versions with vendored bytecode appear; read via {@link tokenArtifact}. - */ -export const TOKEN_ARTIFACTS: Partial> = { - [TokenVersion.V2_0_0]: { - iface: new Interface(CROSS_CHAIN_TOKEN_V2_0_0_ABI), - 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 tokenArtifact(version: TokenVersion): TokenArtifact { - const artifact = TOKEN_ARTIFACTS[version] - if (!artifact) throw new CCTContractVersionUnsupportedError('token', version) - return artifact -} diff --git a/ccip-sdk/src/cct/evm/validate.ts b/ccip-sdk/src/cct/evm/validate.ts index 279f2e2aa..5740169a4 100644 --- a/ccip-sdk/src/cct/evm/validate.ts +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -5,19 +5,23 @@ * @packageDocumentation */ -import { isAddress } from 'ethers' +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. Links the canonical - * {@link CCIPAddressInvalidError} as the `cause`, keeping the + * 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): void { +export function validateAddress( + operation: string, + param: string, + value: unknown, +): asserts value is string { if (typeof value === 'string' && isAddress(value)) return throw new CCTParamsInvalidError( operation, @@ -29,6 +33,18 @@ export function validateAddress(operation: string, param: string, value: unknown ) } +/** + * 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 From b8ae71be4a244a14aeb6a77569910a3ec922dbe5 Mon Sep 17 00:00:00 2001 From: Mervin Date: Tue, 4 Aug 2026 20:07:13 +0800 Subject: [PATCH 49/87] feat(cct-sdk): Add get token admin registry config solana op (#318) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * feat: add accept admin op solana * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments * fix: address comments * fix: address comments * fix: update err.context.reason test * feat: add get token admin registry config op solana * fix: group tests * fix: merge conflicts * fix: remove console logs * fix: refactor test * fix: add tsdoc for decodeTokenAdminRegistryConfig --- ccip-sdk/src/cct/solana/index.ts | 28 ++- .../get-token-admin-registry.test.ts | 155 ++++++++++++++++ .../operations/get-token-admin-registry.ts | 56 ++++++ .../token-admin-registry/operations/index.ts | 1 + .../operations/get-token-pool-state.test.ts | 170 +++++++++--------- ccip-sdk/src/solana/__tests__/index.test.ts | 73 ++++++++ ccip-sdk/src/solana/index.ts | 76 +++----- ccip-sdk/src/solana/token-admin-registry.ts | 103 +++++++++++ 8 files changed, 527 insertions(+), 135 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/get-token-admin-registry.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/get-token-admin-registry.ts create mode 100644 ccip-sdk/src/solana/token-admin-registry.ts diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 71ea5ba82..62e35c99c 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -8,7 +8,7 @@ import type { Connection } from '@solana/web3.js' import type { ChainContext } from '../../chain.ts' import type { ChainFamily } from '../../networks.ts' -import { SolanaChain } from '../../solana/index.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' @@ -48,9 +48,12 @@ import { type GenerateSetPoolResult, type GenerateTransferAdminParams, type GenerateTransferAdminResult, + type GetTokenAdminRegistryParams, + type GetTokenAdminRegistryResult, AcceptAdmin, AppendToLookupTable, CreateLookupTable, + GetTokenAdminRegistry, RegisterAdmin, SetPool, TransferAdmin, @@ -81,6 +84,7 @@ export class SolanaTokenManager extends TokenManager readonly #acceptAdmin = new AcceptAdmin() readonly #appendToLookupTable = new AppendToLookupTable() readonly #createLookupTable = new CreateLookupTable() + readonly #getTokenAdminRegistry = new GetTokenAdminRegistry() readonly #registerAdmin = new RegisterAdmin() readonly #setPool = new SetPool() readonly #transferAdmin = new TransferAdmin() @@ -103,11 +107,13 @@ export class SolanaTokenManager extends TokenManager /** 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)) } @@ -733,6 +739,26 @@ export class SolanaTokenManager extends TokenManager 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) + } + /** * Serializes an unsigned Solana CCT tx for external signing. * 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 000000000..848845b78 --- /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('Solana TokenAdminRegistry getTokenAdminRegistry', () => { + 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 000000000..66ff12506 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-token-admin-registry.ts @@ -0,0 +1,56 @@ +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 +} + +/** Reads a token's TokenAdminRegistry account. */ +export class GetTokenAdminRegistry extends SolanaQuery< + GetTokenAdminRegistryParams, + GetTokenAdminRegistryResult +> { + /** Reads and serializes the TokenAdminRegistry account. */ + async query( + chain: SolanaChain, + params: GetTokenAdminRegistryParams, + ): Promise { + validatePublicKey(this.constructor.name, 'address', params.address) + + const router = new PublicKey(await chain.getTokenAdminRegistryFor(params.address)) + const tokenMint = parsePublicKey(this.constructor.name, 'tokenAddress', params.tokenAddress) + const config = await getTokenAdminRegistryConfig(chain.connection, router, 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 index dfcd66949..c18d83355 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -1,6 +1,7 @@ export * from './accept-admin.ts' export * from './append-to-lookup-table.ts' export * from './create-lookup-table.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-pool/operations/get-token-pool-state.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-state.test.ts index ffa94cb59..862e93371 100644 --- 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 @@ -37,94 +37,104 @@ function stateData(mint: PublicKey): Buffer { } describe('Solana token pool getTokenPoolState', () => { - 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 + 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(), - }) + 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) - 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()) - }) + assert.equal(lockRelease.version, 1) + assert.equal(lockRelease.config.mint, mint.toBase58()) + assert.equal(lockRelease.config.decimals, 6) + 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 + 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.match(error.message, /^Unable to decode token pool state at /) - assert.equal(error.context.mint, mint) - assert.equal(error.context.poolProgram, poolProgram) - assert.ok(error.cause instanceof Error) - return true - }, - ) - }) + await assert.rejects( + new GetTokenPoolState().query(chain, { + tokenAddress: mint, + poolProgramAddress: poolProgram, + }), + (error: unknown) => { + assert.ok(error instanceof CCTDataDecodeError) + assert.match(error.message, /^Unable to decode token pool state at /) + assert.equal(error.context.mint, mint) + assert.equal(error.context.poolProgram, poolProgram) + assert.ok(error.cause instanceof Error) + 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 + 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 - }, - ) + 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 + }, + ) + }) }) - it('requires exactly one pool program reference', async () => { - const getTokenPoolState = new GetTokenPoolState() - const tokenAddress = key(2).toBase58() - const poolProgramAddress = key(15).toBase58() + 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)) + 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/solana/__tests__/index.test.ts b/ccip-sdk/src/solana/__tests__/index.test.ts index 8e9a5d2b2..80c18e0d4 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 @@ -606,3 +610,72 @@ describe('SolanaChain.encodeExtraArgs', () => { assert.equal(parsed?._tag, 'EVMExtraArgsV2') }) }) + +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(), + }) + }) +}) diff --git a/ccip-sdk/src/solana/index.ts b/ccip-sdk/src/solana/index.ts index 1cfa6f108..05e89e54c 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 bs58 from 'bs58' import { @@ -56,7 +55,6 @@ import { CCIPSplTokenInvalidError, CCIPTokenAccountNotFoundError, CCIPTokenDataParseError, - CCIPTokenNotConfiguredError, CCIPTokenPoolChainConfigNotFoundError, CCIPTokenPoolStateNotFoundError, CCIPTopicsInvalidError, @@ -122,6 +120,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 { type CCIPMessage_V1_6_Solana, type UnsignedSolanaTx, isWallet } from './types.ts' import { convertRateLimiter, @@ -1636,49 +1638,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 } /** @@ -1834,8 +1802,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_, { @@ -1848,14 +1814,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 000000000..335ad272a --- /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 +} From fac6e77744e7d02176ca5ba8691d5b5e2950b707 Mon Sep 17 00:00:00 2001 From: Mervin Date: Tue, 4 Aug 2026 21:29:25 +0800 Subject: [PATCH 50/87] feat(cct-sdk): Add get supported tokens solana op (#319) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * feat: add accept admin op solana * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments * fix: address comments * fix: address comments * fix: update err.context.reason test * feat: add get token admin registry config op solana * fix: group tests * fix: merge conflicts * fix: remove console logs * fix: refactor test * fix: add tsdoc for decodeTokenAdminRegistryConfig * feat: add get supported tokens op solana * fix: address followup comments * fix: address comments --- ccip-sdk/src/cct/errors.ts | 5 ++- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 20 +++++++++ .../src/cct/solana/programs/token-pool.ts | 13 ++++-- .../operations/get-supported-tokens.test.ts | 45 +++++++++++++++++++ .../operations/get-supported-tokens.ts | 23 ++++++++++ .../get-token-admin-registry.test.ts | 2 +- .../token-admin-registry/operations/index.ts | 1 + .../operations/get-token-pool-state.test.ts | 31 ++++++++++++- .../operations/get-token-pool-state.ts | 1 + ccip-sdk/src/errors/errors.test.ts | 7 +++ ccip-sdk/src/errors/recovery.ts | 3 +- 12 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/get-supported-tokens.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/get-supported-tokens.ts diff --git a/ccip-sdk/src/cct/errors.ts b/ccip-sdk/src/cct/errors.ts index 154e850d1..b09992191 100644 --- a/ccip-sdk/src/cct/errors.ts +++ b/ccip-sdk/src/cct/errors.ts @@ -212,10 +212,11 @@ export class CCTOperationUnsupportedError extends CCIPError { export class CCTDataDecodeError extends CCIPError { override readonly name = 'CCTDataDecodeError' /** Creates a CCT data decode error. */ - constructor(message = 'Unable to decode CCT data', options?: CCIPErrorOptions) { - super(CCIPErrorCode.CCT_DATA_DECODE_FAILED, message, { + 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/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 5cad1f36c..2af3c38fc 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -38,6 +38,8 @@ describe('SolanaTokenManager (cct/solana)', () => { 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.generateUnsignedCreateTokenMultisig, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 62e35c99c..406d4dfd9 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -48,11 +48,13 @@ import { type GenerateSetPoolResult, type GenerateTransferAdminParams, type GenerateTransferAdminResult, + type GetSupportedTokensParams, type GetTokenAdminRegistryParams, type GetTokenAdminRegistryResult, AcceptAdmin, AppendToLookupTable, CreateLookupTable, + GetSupportedTokens, GetTokenAdminRegistry, RegisterAdmin, SetPool, @@ -84,6 +86,7 @@ export class SolanaTokenManager extends TokenManager 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() @@ -759,6 +762,23 @@ export class SolanaTokenManager extends TokenManager 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. * diff --git a/ccip-sdk/src/cct/solana/programs/token-pool.ts b/ccip-sdk/src/cct/solana/programs/token-pool.ts index a15b4299c..f1fc4a362 100644 --- a/ccip-sdk/src/cct/solana/programs/token-pool.ts +++ b/ccip-sdk/src/cct/solana/programs/token-pool.ts @@ -3,6 +3,7 @@ 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, @@ -48,6 +49,7 @@ type TokenPoolStateDecodeContext = { tokenPool: string mint: string poolProgram: string + accountOwner: string } /** @@ -77,14 +79,17 @@ export function decodeTokenPoolState( context: TokenPoolStateDecodeContext, ): { version: number; config: TokenPoolConfig } { try { - return tokenPoolCoder.accounts.decode('state', data) + return tokenPoolCoder.accounts.decode<{ version: number; config: TokenPoolConfig }>( + 'state', + data, + ) } catch (cause) { - throw new CCTDataDecodeError(`Unable to decode token pool state at ${context.tokenPool}`, { - cause: cause instanceof Error ? cause : undefined, + throw new CCTDataDecodeError(context.tokenPool, { + cause: cause instanceof Error ? cause : CCIPError.from(cause), context: { - tokenPool: context.tokenPool, mint: context.mint, poolProgram: context.poolProgram, + accountOwner: context.accountOwner, }, }) } 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 000000000..b5868427b --- /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 000000000..2b604eba8 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-supported-tokens.ts @@ -0,0 +1,23 @@ +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 { + /** Resolves the Router and lists its configured token mints. */ + async query(chain: SolanaChain, params: GetSupportedTokensParams): Promise { + validatePublicKey(this.constructor.name, 'address', params.address) + + 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 index 848845b78..23756b46a 100644 --- 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 @@ -56,7 +56,7 @@ function stubChain(account: { data: Buffer } | null = registryAccount()): Solana } as unknown as SolanaChain } -describe('Solana TokenAdminRegistry getTokenAdminRegistry', () => { +describe('GetTokenAdminRegistry (cct/solana)', () => { describe('query', () => { it('returns configured administrators, lookup table, and writable indexes', async () => { const config = await SolanaTokenManager.fromChain(stubChain()).getTokenAdminRegistry({ 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 index c18d83355..0437d089f 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -1,6 +1,7 @@ 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' 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 index 862e93371..3b5adffea 100644 --- 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 @@ -6,8 +6,10 @@ 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)) @@ -86,15 +88,42 @@ describe('Solana token pool getTokenPoolState', () => { }), (error: unknown) => { assert.ok(error instanceof CCTDataDecodeError) - assert.match(error.message, /^Unable to decode token pool state at /) + 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() 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 index 8d9b7b970..93ea6d9ce 100644 --- 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 @@ -130,6 +130,7 @@ export class GetTokenPoolState extends SolanaQuery< tokenPool: state.toBase58(), mint: params.tokenAddress, poolProgram: programId.toBase58(), + accountOwner: account.owner.toBase58(), }) const result = { stateAddress: state.toBase58(), diff --git a/ccip-sdk/src/errors/errors.test.ts b/ccip-sdk/src/errors/errors.test.ts index 98db9aef7..927e89962 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 bab05bd55..725e2947f 100644 --- a/ccip-sdk/src/errors/recovery.ts +++ b/ccip-sdk/src/errors/recovery.ts @@ -109,7 +109,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: From 39af9e8b497f1c62e90b682e154534a4ba277e5e Mon Sep 17 00:00:00 2001 From: Mervin Date: Thu, 6 Aug 2026 18:46:25 +0800 Subject: [PATCH 51/87] fix(cct-sdk): Update and refactor solana lifecycle and tests (#321) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * feat: add accept admin op solana * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments * fix: address comments * fix: address comments * fix: update err.context.reason test * feat: add get token admin registry config op solana * fix: group tests * fix: merge conflicts * fix: remove console logs * fix: refactor test * fix: add tsdoc for decodeTokenAdminRegistryConfig * feat: add get supported tokens op solana * fix: address followup comments * fix: address comments * fix: add validateOptionalPublicKey and refactor test files * fix: update solana lifecycle * fix: remove dev build * fix: address comments * fix: params.additionalAddresses.length with eslint rule * fix: add TODO and validation test --- ccip-sdk/package.json | 5 +- ccip-sdk/src/cct/solana/operation.test.ts | 56 ++++++ ccip-sdk/src/cct/solana/operation.ts | 37 +++- .../operations/accept-admin.ts | 58 +++--- .../operations/append-to-lookup-table.ts | 18 +- .../operations/create-lookup-table.ts | 5 +- .../operations/register-admin.ts | 15 +- .../operations/set-pool.ts | 8 +- .../operations/transfer-admin.ts | 10 +- .../operations/create-token-multisig.test.ts | 168 ++++++++-------- .../operations/deploy-token-pool.test.ts | 152 ++++++++------- .../operations/deploy-token-pool.ts | 5 +- .../operations/get-token-pool-state.test.ts | 2 +- .../operations/create-token-account.test.ts | 95 +++++---- .../token/operations/deploy-token.test.ts | 184 +++++++++--------- .../solana/token/operations/deploy-token.ts | 7 +- ccip-sdk/src/cct/solana/validate.test.ts | 17 ++ ccip-sdk/src/cct/solana/validate.ts | 17 +- ccip-sdk/tsconfig.build.dev.json | 4 - ccip-sdk/tsconfig.build.json | 12 +- 20 files changed, 524 insertions(+), 351 deletions(-) delete mode 100644 ccip-sdk/tsconfig.build.dev.json diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index 5ff6886b1..d3f5e1fad 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -45,7 +45,6 @@ "typecheck": "tsc --noEmit", "check": "npm run lint && npm run typecheck", "build": "npm run clean && tsc -p ./tsconfig.build.json", - "build:dev": "npm run clean && tsc -p ./tsconfig.build.dev.json", "clean": "rm -rfv ./dist", "prepare": "npm run build" }, @@ -55,9 +54,7 @@ "tsconfig.json", "!**/*.test.*", "!**/__tests__", - "!**/__mocks__", - "!dist/cct/**", - "!src/cct/**" + "!**/__mocks__" ], "peerDependencies": { "viem": "^2.0.0" diff --git a/ccip-sdk/src/cct/solana/operation.test.ts b/ccip-sdk/src/cct/solana/operation.test.ts index 499c9273a..54e45c452 100644 --- a/ccip-sdk/src/cct/solana/operation.test.ts +++ b/ccip-sdk/src/cct/solana/operation.test.ts @@ -27,9 +27,65 @@ class TestOperation extends SolanaOperation<{ value: string }> { } } +class ParsedTestOperation extends SolanaOperation< + { value: string }, + UnsignedSolanaTx, + { payer: string; value: number } +> { + readonly name = 'parsedTestOperation' + readonly lifecycle: string[] = [] + captured?: { payer: string; value: number } + + protected 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 = { diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts index 3afbdfcc6..f6a216857 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -1,5 +1,5 @@ /** - * Solana {@link Operation} lifecycle: validate → build unsigned tx → submit. + * 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 @@ -28,18 +28,41 @@ function withPayer

( return { ...rest, payer } as SolanaGenerateParams

} -/** Solana CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. */ +// TODO: migrate remaining Solana operations to parse normalized params. +/** + * Solana CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. + * + * Use {@link validate} for cross-field constraints. Override {@link parse} for per-field + * 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> { - /** Build instructions after params have been validated. */ - protected abstract buildUnsigned(chain: SolanaChain, params: SolanaGenerateParams

): Promise + /** + * 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 + } - /** Run {@link validate} and {@link buildUnsigned}; no signing. */ - async generate(chain: SolanaChain, params: SolanaGenerateParams

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

): Parsed { this.validate(params) - return this.buildUnsigned(chain, 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)) } /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ 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 index bc4d3a99f..f7a64e841 100644 --- 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 @@ -17,7 +17,7 @@ import { deriveTokenAdminRegistryPda, } from '../../programs/router.ts' import { submit } from '../../submit.ts' -import { validateAuthorityMatchesWallet, validatePublicKey } from '../../validate.ts' +import { parsePublicKey, validateAuthorityMatchesWallet } from '../../validate.ts' /** Parameters shared by Solana TokenAdminRegistry `acceptAdmin` generation and execution. */ type AcceptAdminParams = { @@ -31,6 +31,13 @@ type AcceptAdminParams = { authority?: string } +type ParsedAcceptAdminParams = { + tokenAddress: PublicKey + address: PublicKey + payer: PublicKey + authority: PublicKey +} + /** Parameters for unsigned Solana TokenAdminRegistry `acceptAdmin` generation. */ export type GenerateAcceptAdminParams = SolanaGenerateParams @@ -44,26 +51,37 @@ export type ExecuteAcceptAdminParams = SolanaExecuteParams export type ExecuteAcceptAdminResult = TransactionResult /** Accepts a pending TokenAdminRegistry administrator role. */ -export class AcceptAdmin extends SolanaOperation { +export class AcceptAdmin extends SolanaOperation< + AcceptAdminParams, + UnsignedSolanaTx, + ParsedAcceptAdminParams +> { readonly name = 'acceptAdmin' - /** Validates all public keys before any RPC. */ - protected validate(params: GenerateAcceptAdminParams): void { - validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) - validatePublicKey(this.name, 'address', params.address) - validatePublicKey(this.name, 'payer', params.payer) - if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + /** No cross-field constraints; {@link parse} validates individual public-key parameters. */ + protected validate(_params: GenerateAcceptAdminParams): void {} + + /** 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: GenerateAcceptAdminParams, + opts: ParsedAcceptAdminParams, ): Promise { - const tokenMint = new PublicKey(opts.tokenAddress) - const payer = new PublicKey(opts.payer) - const authority = new PublicKey(opts.authority ?? opts.payer) - const router = new PublicKey(await chain.getTokenAdminRegistryFor(opts.address)) + 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) { @@ -107,23 +125,17 @@ export class AcceptAdmin extends SolanaOperation { const payer = wallet.publicKey.toBase58() const generateParams: GenerateAcceptAdminParams = { ...rest, payer } - this.validate(generateParams) + const parsed = this.prepare(generateParams) - if (params.authority) { + if (params.authority !== undefined) { validateAuthorityMatchesWallet( this.name, - new PublicKey(params.authority), + 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, generateParams), - this.name, - computeUnits, - ) + 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.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/append-to-lookup-table.ts index e98b90bfe..d2c6decad 100644 --- 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 @@ -17,6 +17,7 @@ import { submit } from '../../submit.ts' import { resolvePoolProgram, validateAuthorityMatchesWallet, + validateOptionalPublicKey, validatePublicKey, } from '../../validate.ts' @@ -72,24 +73,26 @@ export class AppendToLookupTable extends SolanaOperation< protected validate(params: GenerateAppendToLookupTableParams): void { validatePublicKey(this.name, 'lookupTableAddress', params.lookupTableAddress) validatePublicKey(this.name, 'payer', params.payer) - if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateOptionalPublicKey(this.name, 'authority', params.authority) + const hasTokenAddress = params.tokenAddress !== undefined const hasPoolProgramAddress = params.poolProgramAddress !== undefined const hasPoolProgram = params.poolType !== undefined || hasPoolProgramAddress - if (Boolean(params.tokenAddress) !== hasPoolProgram) { + if (hasTokenAddress !== hasPoolProgram) { throw new CCTParamsInvalidError( this.name, 'tokenAddress', 'tokenAddress and exactly one of poolType or poolProgramAddress must be provided together', ) } - if (params.tokenAddress) validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + validateOptionalPublicKey(this.name, 'tokenAddress', params.tokenAddress) if (hasPoolProgram) resolvePoolProgram(this.name, params) for (const [i, address] of (params.additionalAddresses ?? []).entries()) { validatePublicKey(this.name, `additionalAddresses[${i}]`, address) } - if (!params.tokenAddress && !params.additionalAddresses?.length) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (params.tokenAddress === undefined && !params.additionalAddresses?.length) { throw new CCTParamsInvalidError( this.name, 'additionalAddresses', @@ -106,7 +109,8 @@ export class AppendToLookupTable extends SolanaOperation< const payer = new PublicKey(opts.payer) const authority = new PublicKey(opts.authority ?? opts.payer) const lookupTableAddress = new PublicKey(opts.lookupTableAddress) - const poolProgram = opts.tokenAddress ? resolvePoolProgram(this.name, opts) : undefined + const poolProgram = + opts.tokenAddress !== undefined ? resolvePoolProgram(this.name, opts) : undefined const lookupTable = await chain.connection.getAddressLookupTable(lookupTableAddress) if (!lookupTable.value) { @@ -127,7 +131,7 @@ export class AppendToLookupTable extends SolanaOperation< const addresses = [...(opts.additionalAddresses ?? []).map((a) => new PublicKey(a))] - if (opts.tokenAddress && poolProgram) { + if (opts.tokenAddress !== undefined && poolProgram) { const tokenMint = new PublicKey(opts.tokenAddress) const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { lookupTableAddress, @@ -192,7 +196,7 @@ export class AppendToLookupTable extends SolanaOperation< const generateParams: GenerateAppendToLookupTableParams = { ...rest, payer } this.validate(generateParams) - const authority = params.authority ? new PublicKey(params.authority) : undefined + const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined if (authority) { validateAuthorityMatchesWallet( this.name, 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 index 2dd592ac0..567dfd35d 100644 --- 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 @@ -20,6 +20,7 @@ import { submit } from '../../submit.ts' import { resolvePoolProgram, validateAuthorityMatchesWallet, + validateOptionalPublicKey, validatePublicKey, } from '../../validate.ts' @@ -69,7 +70,7 @@ export class CreateLookupTable extends SolanaOperation< /** Validates params before `buildUnsigned()` performs any RPC. */ protected validate(params: GenerateCreateLookupTableParams): void { validatePublicKey(this.name, 'payer', params.payer) - if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateOptionalPublicKey(this.name, 'authority', params.authority) if (params.mode === 'createEmpty') return validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) @@ -166,7 +167,7 @@ export class CreateLookupTable extends SolanaOperation< this.validate(generateParams) - const authority = params.authority ? new PublicKey(params.authority) : undefined + const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined if (params.mode !== 'createEmpty' && authority) { validateAuthorityMatchesWallet( this.name, 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 index db0dc9db9..e3276fbe8 100644 --- 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 @@ -19,7 +19,11 @@ import { deriveTokenAdminRegistryPda, } from '../../programs/router.ts' import { submit } from '../../submit.ts' -import { validateAuthorityMatchesWallet, validatePublicKey } from '../../validate.ts' +import { + validateAuthorityMatchesWallet, + validateOptionalPublicKey, + validatePublicKey, +} from '../../validate.ts' /** Authorization paths used to register a token in the TokenAdminRegistry. */ const REGISTER_ADMIN_METHODS = { @@ -135,8 +139,8 @@ export class RegisterAdmin extends SolanaOperation { validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) validatePublicKey(this.name, 'address', params.address) validatePublicKey(this.name, 'payer', params.payer) - if (params.administrator) validatePublicKey(this.name, 'administrator', params.administrator) - if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateOptionalPublicKey(this.name, 'administrator', params.administrator) + validateOptionalPublicKey(this.name, 'authority', params.authority) if ( params.registrationMethod !== undefined && !Object.values(REGISTER_ADMIN_METHODS).includes(params.registrationMethod) @@ -162,7 +166,8 @@ export class RegisterAdmin extends SolanaOperation { const mintAccount = await resolveTokenMint(chain.connection, tokenMint) const { mintAuthority } = unpackMint(tokenMint, mintAccount, mintAccount.owner) - const administrator = opts.administrator ? new PublicKey(opts.administrator) : mintAuthority + const administrator = + opts.administrator !== undefined ? new PublicKey(opts.administrator) : mintAuthority const method = opts.registrationMethod ?? REGISTER_ADMIN_METHODS.OWNER const config = deriveRouterConfigPda(router) const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, tokenMint) @@ -224,7 +229,7 @@ export class RegisterAdmin extends SolanaOperation { const generateParams: GenerateRegisterAdminParams = { ...rest, payer } this.validate(generateParams) - const authority = params.authority ? new PublicKey(params.authority) : undefined + const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined if (authority) { validateAuthorityMatchesWallet( this.name, 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 index 651877f5f..4233c0eb5 100644 --- 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 @@ -16,7 +16,11 @@ import { deriveRouterConfigPda, deriveTokenAdminRegistryPda, } from '../../programs/router.ts' -import { validatePublicKey, validateWritableIndexes } from '../../validate.ts' +import { + validateOptionalPublicKey, + validatePublicKey, + validateWritableIndexes, +} from '../../validate.ts' /** Standard BurnMint/LockRelease pool ALT writable positions. */ export const DEFAULT_WRITABLE_INDEXES = [3, 4, 7] as const @@ -67,7 +71,7 @@ export class SetPool extends SolanaOperation { validatePublicKey(this.name, 'address', params.address) validatePublicKey(this.name, 'poolLookupTableAddress', params.poolLookupTableAddress) validatePublicKey(this.name, 'payer', params.payer) - if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateOptionalPublicKey(this.name, 'authority', params.authority) validateWritableIndexes(this.name, 'writableIndexes', params.writableIndexes) } 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 index b6c855de0..eb710a349 100644 --- 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 @@ -17,7 +17,11 @@ import { deriveTokenAdminRegistryPda, } from '../../programs/router.ts' import { submit } from '../../submit.ts' -import { validateAuthorityMatchesWallet, validatePublicKey } from '../../validate.ts' +import { + validateAuthorityMatchesWallet, + validateOptionalPublicKey, + validatePublicKey, +} from '../../validate.ts' /** Parameters shared by Solana TokenAdminRegistry `transferAdmin` generation and execution. */ type TransferAdminParams = { @@ -55,7 +59,7 @@ export class TransferAdmin extends SolanaOperation { validatePublicKey(this.name, 'address', params.address) validatePublicKey(this.name, 'newAdmin', params.newAdmin) validatePublicKey(this.name, 'payer', params.payer) - if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateOptionalPublicKey(this.name, 'authority', params.authority) } /** Builds the unsigned instruction after confirming the caller is the current admin. */ @@ -109,7 +113,7 @@ export class TransferAdmin extends SolanaOperation { const generateParams: GenerateTransferAdminParams = { ...rest, payer } this.validate(generateParams) - if (params.authority) { + if (params.authority !== undefined) { validateAuthorityMatchesWallet( this.name, new PublicKey(params.authority), 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 index 46456dc5d..02c53625b 100644 --- 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 @@ -65,97 +65,103 @@ function generate(opts = {}) { }) } -describe('Solana token createTokenMultisig', () => { - it('builds a pool-autonomous threshold-two multisig', async () => { - const unsigned = await generate({ - threshold: 2, - additionalSigners: [Keypair.generate().publicKey.toBase58()], +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)))) }) - 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)) + 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, - ) - }) + 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()] }) + 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))) + assert.ok(unsigned.instructions[1]!.keys.some((key) => key.pubkey.equals(signer))) + }) }) - 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', - ) - }) + 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('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', - ) + 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', + ) + }) }) - 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', - ) + 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/deploy-token-pool.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/deploy-token-pool.test.ts index 309e42654..2c970ae30 100644 --- 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 @@ -40,84 +40,100 @@ function generate(opts = {}) { }) } -describe('Solana token pool deployTokenPool', () => { - 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()], +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(), + ) }) - assert.equal(unsigned.instructions.length, 2) - assert.equal(unsigned.instructions[1]!.programId.toBase58(), BURN_MINT_POOL_PROGRAM) - }) + it('adds configure allowlist instruction when provided', async () => { + const unsigned = await generate({ + allowlist: [Keypair.generate().publicKey.toBase58()], + }) - it('uses canonical lock-release pool program', async () => { - const unsigned = await generate({ poolType: 'lock-release' }) + assert.equal(unsigned.instructions.length, 2) + assert.equal(unsigned.instructions[1]!.programId.toBase58(), BURN_MINT_POOL_PROGRAM) + }) - assert.equal(unsigned.instructions[0]!.programId.toBase58(), LOCK_RELEASE_POOL_PROGRAM) - }) + it('uses canonical lock-release pool program', async () => { + const unsigned = await generate({ poolType: 'lock-release' }) - it('defaults authority to payer', async () => { - const unsigned = await generate({ authority: undefined }) + assert.equal(unsigned.instructions[0]!.programId.toBase58(), LOCK_RELEASE_POOL_PROGRAM) + }) - assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) - }) + it('defaults authority to payer', async () => { + const unsigned = await generate({ authority: undefined }) - 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', - ) + assert.ok(unsigned.instructions[0]!.keys.some((key) => key.pubkey.toBase58() === PAYER)) + }) }) - 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', - ) + 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]', + ) + }) }) - 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 index 95508b158..1712fb47f 100644 --- 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 @@ -22,6 +22,7 @@ import { import { submit } from '../../submit.ts' import { validateAuthorityMatchesWallet, + validateOptionalPublicKey, validatePoolType, validatePublicKey, validatePublicKeys, @@ -81,7 +82,7 @@ export class DeployTokenPool extends SolanaOperation< validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) validatePoolType(this.name, 'poolType', params.poolType) validatePublicKey(this.name, 'payer', params.payer) - if (params.authority) validatePublicKey(this.name, 'authority', params.authority) + validateOptionalPublicKey(this.name, 'authority', params.authority) if (params.allowlist !== undefined) validatePublicKeys(this.name, 'allowlist', params.allowlist) } @@ -152,7 +153,7 @@ export class DeployTokenPool extends SolanaOperation< const generateParams: GenerateDeployTokenPoolParams = { ...rest, payer } this.validate(generateParams) - const authority = params.authority ? new PublicKey(params.authority) : undefined + const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined if (authority) { validateAuthorityMatchesWallet( this.name, 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 index 3b5adffea..515e2761e 100644 --- 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 @@ -38,7 +38,7 @@ function stateData(mint: PublicKey): Buffer { ]) } -describe('Solana token pool getTokenPoolState', () => { +describe('GetTokenPoolState (cct/solana)', () => { describe('query', () => { it('returns decoded state fields', async () => { const mint = key(2) 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 index 00fbfd59d..c36517b84 100644 --- 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 @@ -9,7 +9,11 @@ import { } from '@solana/spl-token' import { type PublicKey, Keypair } from '@solana/web3.js' -import { CCIPTokenMintInvalidError, CCIPTokenMintNotFoundError } from '../../../../errors/index.ts' +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' @@ -36,48 +40,65 @@ function generate(opts = {}, mintOwner?: PublicKey | null) { }) } -describe('Solana token createTokenAccount', () => { - 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) +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()) - }) + 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) + 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(), - ) + assert.equal(unsigned.tokenAccountAddress, ata.toBase58()) + assert.equal( + unsigned.instructions[0]!.keys.at(-1)!.pubkey.toBase58(), + TOKEN_PROGRAM_ID.toBase58(), + ) + }) }) - it('rejects a missing mint', async () => { - await assert.rejects( - () => generate({}, null), - (err: unknown) => err instanceof CCIPTokenMintNotFoundError, - ) + 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, + ) + }) }) - 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/deploy-token.test.ts b/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts index a2a887a5a..b891c8664 100644 --- a/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts @@ -34,108 +34,114 @@ function generate(opts = {}) { }) } -describe('Solana token deployToken', () => { - 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' }) +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 + }) - assert.equal(a.tokenAddress, b.tokenAddress) - }) + it('uses caller seed for reproducible mint address', async () => { + const a = await generate({ seed: 'mint_seed' }) + const b = await generate({ seed: 'mint_seed' }) - it('adds Metaplex metadata when requested', async () => { - const unsigned = await generate({ - withMetaplex: true, - name: 'My Token', - symbol: 'MTK', + assert.equal(a.tokenAddress, b.tokenAddress) }) - 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('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', + 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(), + ), + ) }) - 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(), + }) - 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 }) - - assert.equal(unsigned.instructions.length, 4) - assert.equal(unsigned.instructions[3]!.data[0], 7) // MintTo }) - 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('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', + ) + }) }) - 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', - ) - }) + 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', - ) + 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 index 718bd95fe..5c6868aad 100644 --- a/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts @@ -21,7 +21,7 @@ import { SolanaOperation, } from '../../operation.ts' import { submit } from '../../submit.ts' -import { validatePublicKey } from '../../validate.ts' +import { validateOptionalPublicKey, validatePublicKey } from '../../validate.ts' type BaseDeployTokenParams = { /** Mint decimals. Must be an integer between 0 and 255. */ @@ -248,7 +248,7 @@ function validateBaseParams(operation: string, params: GenerateDeployTokenParams if (params.seed !== undefined && (!params.seed || utf8ByteLength(params.seed) > 32)) { throw new CCTParamsInvalidError(operation, 'seed', 'must be non-empty and <= 32 UTF-8 bytes') } - if (params.mintAuthority) validatePublicKey(operation, 'mintAuthority', params.mintAuthority) + validateOptionalPublicKey(operation, 'mintAuthority', params.mintAuthority) if (params.freezeAuthority !== undefined && params.freezeAuthority !== null) { validatePublicKey(operation, 'freezeAuthority', params.freezeAuthority) } @@ -268,8 +268,7 @@ function validatePreMintParams(operation: string, params: GenerateDeployTokenPar 'is required when preMint is set', ) } - if (params.preMintRecipient) - validatePublicKey(operation, 'preMintRecipient', params.preMintRecipient) + validateOptionalPublicKey(operation, 'preMintRecipient', params.preMintRecipient) } function validateMetaplexParams(operation: string, params: GenerateDeployTokenParams): void { diff --git a/ccip-sdk/src/cct/solana/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts index ad0a2c9ec..02251b11c 100644 --- a/ccip-sdk/src/cct/solana/validate.test.ts +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -8,6 +8,7 @@ import { resolvePoolProgram, validateInteger, validateNonEmptyString, + validateOptionalPublicKey, validatePoolType, validatePublicKey, validatePublicKeys, @@ -26,6 +27,22 @@ describe('Validate (cct/solana)', () => { 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), diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index c9f47b278..00be84099 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -19,7 +19,7 @@ export function parsePublicKey(operation: string, param: string, value: unknown) throw new CCTParamsInvalidError( operation, param, - `must be a valid Solana public key, got ${String(value)}`, + `must be a valid Solana public key, got "${String(value)}"`, ) } @@ -29,7 +29,7 @@ export function parsePublicKey(operation: string, param: string, value: unknown) throw new CCTParamsInvalidError( operation, param, - `must be a valid Solana public key, got ${String(value)}`, + `must be a valid Solana public key, got "${String(value)}"`, { cause: new CCIPAddressInvalidError(value, ChainFamily.Solana), }, @@ -49,6 +49,19 @@ export function validatePublicKey( 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. diff --git a/ccip-sdk/tsconfig.build.dev.json b/ccip-sdk/tsconfig.build.dev.json deleted file mode 100644 index cec1e7dbb..000000000 --- a/ccip-sdk/tsconfig.build.dev.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.build.json", - "exclude": ["node_modules", "**/*.test.*", "**/__tests__", "**/__mocks__"] -} diff --git a/ccip-sdk/tsconfig.build.json b/ccip-sdk/tsconfig.build.json index eba9f19f5..78779ee5b 100644 --- a/ccip-sdk/tsconfig.build.json +++ b/ccip-sdk/tsconfig.build.json @@ -4,14 +4,6 @@ "outDir": "./dist", "rootDir": "./src" }, - "include": [ - "./src" - ], - "exclude": [ - "node_modules", - "**/*.test.*", - "**/__tests__", - "**/__mocks__", - "./src/cct" - ] + "include": ["./src"], + "exclude": ["node_modules", "**/*.test.*", "**/__tests__", "**/__mocks__"] } From 1af73ba45c819be2bfe4880adf7f251d0167f71e Mon Sep 17 00:00:00 2001 From: Mervin Date: Thu, 6 Aug 2026 19:46:25 +0800 Subject: [PATCH 52/87] feat(cct-sdk): Add configure allowlist solana op (#323) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * feat: add accept admin op solana * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments * fix: address comments * fix: address comments * fix: update err.context.reason test * feat: add get token admin registry config op solana * fix: group tests * fix: merge conflicts * fix: remove console logs * fix: refactor test * fix: add tsdoc for decodeTokenAdminRegistryConfig * feat: add get supported tokens op solana * fix: address followup comments * fix: address comments * fix: add validateOptionalPublicKey and refactor test files * fix: update solana lifecycle * fix: remove dev build * feat: add configure allowlist op solana * fix: assert configure allowlist functions * fix: address comments * fix: params.additionalAddresses.length with eslint rule * fix: address comments --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 73 ++++++ .../operations/configure-allowlist.test.ts | 211 ++++++++++++++++++ .../operations/configure-allowlist.ts | 151 +++++++++++++ .../cct/solana/token-pool/operations/index.ts | 1 + 5 files changed, 438 insertions(+) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 2af3c38fc..cf90eb838 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -42,6 +42,8 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(typeof cct.getSupportedTokens, 'function') // Token pool operations + 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') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 406d4dfd9..ce86c12a7 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -61,16 +61,21 @@ import { TransferAdmin, } from './token-admin-registry/operations/index.ts' import { + type ExecuteConfigureAllowlistParams, + type ExecuteConfigureAllowlistResult, type ExecuteCreateTokenMultisigParams, type ExecuteCreateTokenMultisigResult, type ExecuteDeployTokenPoolParams, type ExecuteDeployTokenPoolResult, + type GenerateConfigureAllowlistParams, + type GenerateConfigureAllowlistResult, type GenerateCreateTokenMultisigParams, type GenerateCreateTokenMultisigResult, type GenerateDeployTokenPoolParams, type GenerateDeployTokenPoolResult, type GetTokenPoolStateParams, type GetTokenPoolStateResult, + ConfigureAllowlist, CreateTokenMultisig, DeployTokenPool, GetTokenPoolState, @@ -93,6 +98,7 @@ export class SolanaTokenManager extends TokenManager readonly #transferAdmin = new TransferAdmin() // Token pool operations + readonly #configureAllowlist = new ConfigureAllowlist() readonly #createTokenMultisig = new CreateTokenMultisig() readonly #deployTokenPool = new DeployTokenPool() readonly #getTokenPoolState = new GetTokenPoolState() @@ -344,6 +350,73 @@ export class SolanaTokenManager extends TokenManager 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`. + * + * @remarks Removing addresses is not yet supported by the SDK. + * + * @see {@link configureAllowlist} + * @see {@link generateUnsignedDeployTokenPool} + * + * @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. + * + * @remarks Removing addresses is not yet supported by the SDK. + * + * @see {@link generateUnsignedConfigureAllowlist} + * @see {@link deployTokenPool} + * + * @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. * 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 000000000..280b4ba0e --- /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 000000000..f935546bd --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.ts @@ -0,0 +1,151 @@ +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' + + /** {@link parse} validates and normalizes all parameters. */ + protected validate(_params: GenerateConfigureAllowlistParams): void {} + + /** 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/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index 6e8f7f4e1..f8e06e130 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -1,3 +1,4 @@ +export * from './configure-allowlist.ts' export * from './create-token-multisig.ts' export * from './deploy-token-pool.ts' export * from './get-token-pool-state.ts' From fe40a9c29d5820dd48a6d6d436e67f0d56922de7 Mon Sep 17 00:00:00 2001 From: Mervin Date: Thu, 6 Aug 2026 23:21:03 +0800 Subject: [PATCH 53/87] feat(cct-sdk): Add remove from allowlist solana op (#329) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * feat: add accept admin op solana * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments * fix: address comments * fix: address comments * fix: update err.context.reason test * feat: add get token admin registry config op solana * fix: group tests * fix: merge conflicts * fix: remove console logs * fix: refactor test * fix: add tsdoc for decodeTokenAdminRegistryConfig * feat: add get supported tokens op solana * fix: address followup comments * fix: address comments * fix: add validateOptionalPublicKey and refactor test files * fix: update solana lifecycle * fix: remove dev build * feat: add configure allowlist op solana * fix: assert configure allowlist functions * fix: address comments * fix: params.additionalAddresses.length with eslint rule * fix: address comments * feat: add remove from allowlist op solana * fix: add validations and test coverage * fix: add additional tsdoc --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 76 ++++++- .../cct/solana/token-pool/operations/index.ts | 1 + .../operations/remove-from-allowlist.test.ts | 193 ++++++++++++++++++ .../operations/remove-from-allowlist.ts | 148 ++++++++++++++ 5 files changed, 416 insertions(+), 4 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/remove-from-allowlist.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/remove-from-allowlist.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index cf90eb838..ab9399e37 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -48,6 +48,8 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(typeof cct.createTokenMultisig, 'function') assert.equal(typeof cct.generateUnsignedDeployTokenPool, 'function') assert.equal(typeof cct.deployTokenPool, 'function') + assert.equal(typeof cct.generateUnsignedRemoveFromAllowlist, 'function') + assert.equal(typeof cct.removeFromAllowlist, 'function') assert.equal(typeof cct.getTokenPoolState, 'function') }) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index ce86c12a7..89aa0cb19 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -67,18 +67,23 @@ import { type ExecuteCreateTokenMultisigResult, type ExecuteDeployTokenPoolParams, type ExecuteDeployTokenPoolResult, + type ExecuteRemoveFromAllowlistParams, + type ExecuteRemoveFromAllowlistResult, type GenerateConfigureAllowlistParams, type GenerateConfigureAllowlistResult, type GenerateCreateTokenMultisigParams, type GenerateCreateTokenMultisigResult, type GenerateDeployTokenPoolParams, type GenerateDeployTokenPoolResult, + type GenerateRemoveFromAllowlistParams, + type GenerateRemoveFromAllowlistResult, type GetTokenPoolStateParams, type GetTokenPoolStateResult, ConfigureAllowlist, CreateTokenMultisig, DeployTokenPool, GetTokenPoolState, + RemoveFromAllowlist, } from './token-pool/operations/index.ts' /** CCT admin facade for Solana. */ @@ -102,6 +107,7 @@ export class SolanaTokenManager extends TokenManager readonly #createTokenMultisig = new CreateTokenMultisig() readonly #deployTokenPool = new DeployTokenPool() readonly #getTokenPoolState = new GetTokenPoolState() + readonly #removeFromAllowlist = new RemoveFromAllowlist() /** Creates a Solana CCT manager for an existing chain. */ constructor(chain: SolanaChain) { @@ -357,10 +363,9 @@ export class SolanaTokenManager extends TokenManager * program. The pool must be initialized first. Pass canonical `poolType` or a compatible * `poolProgramAddress`; `authority` defaults to `payer`. * - * @remarks Removing addresses is not yet supported by the SDK. - * * @see {@link configureAllowlist} * @see {@link generateUnsignedDeployTokenPool} + * @see {@link generateUnsignedRemoveFromAllowlist} * * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid. * @@ -389,10 +394,9 @@ export class SolanaTokenManager extends TokenManager * appending an address. Addresses in `add` must be unique; existing allowlist entries are * rejected by the program. * - * @remarks Removing addresses is not yet supported by the SDK. - * * @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 @@ -667,6 +671,70 @@ export class SolanaTokenManager extends TokenManager 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. * diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index f8e06e130..0dd6f2a54 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -2,3 +2,4 @@ export * from './configure-allowlist.ts' export * from './create-token-multisig.ts' export * from './deploy-token-pool.ts' export * from './get-token-pool-state.ts' +export * from './remove-from-allowlist.ts' 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 000000000..3e9cb5866 --- /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 000000000..a227648e6 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/remove-from-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 `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' + + /** Validation runs in {@link parse}. */ + protected validate(_params: GenerateRemoveFromAllowlistParams): void {} + + /** 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) + } +} From 2c1f819908a864363ba6181eed90ca4483b95f86 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:36:25 +0100 Subject: [PATCH 54/87] feat(cct-sdk): Add EVM getTokenPoolState op + query read class (#322) * Better separation of concerns and abstractions * CCT: Add version dispatch + Transfer Ownership * Adjust with base * Address generic error types and doc examples * Fix linting * feat(cct-sdk): Add deploy evm token * Address PR comments * Address PR comments by fixing chain-specific types * feat(cct-sdk): Source chainlink/contracts-ccip artifacts (#303) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * Remove outdated bytecodes * feat(cct-sdk): Add CrossChainToken deployment + token versioning (#305) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * feat(cct-sdk): Add versioned Deploy Token (CCT + ERC20) * Remove outdated bytecodes * Deploy only CrossChainToken * Recover previous type changes * Address preMint specs * feat(cct-sdk): Add deploy evm token pool + type/version resolution * add remarks ts doc comment * Address PR comments * add lockfile to fix ci issue * Add comments * feat(cct-sdk): Add EVM deployLockbox operation (DAPP-10738) Vendor ERC20LockBox v2.0.0 artifacts; add the cached lockbox Interface + DeployLockbox op, wire generateUnsignedDeployLockbox/deployLockbox into EVMTokenManager, and document the LockRelease deploy sequence. * feat(cct-sdk): Add EVM authorizeLockboxCallers operation (DAPP-10788) Add AuthorizeLockboxCallers op (wraps ERC20LockBox applyAuthorizedCallerUpdates) + tests, and wire generateUnsignedAuthorizeLockboxCallers/authorizeLockboxCallers into EVMTokenManager to complete the LockRelease flow. * refactor(cct-sdk): add validateNonZeroAddress + guard single-tx submit * feat(cct-sdk): EVM deploy verification + unify deploy ops * Address PR comments * Review pass * feat(cct-sdk): add cross-family Query read base (DAPP-10823) Query wires validate -> read so params are rejected before any RPC, mirroring how Operation.generate gates buildUnsigned on the write side. EVMQuery binds it to an EVMChain and owns getTypedContract, the single ethers -> ethers-abitype bridge that CCT read ops decode through. * feat(cct-sdk): add pool family type guards (DAPP-10823) BurnMintTokenPoolType / LockReleaseTokenPoolType split TOKEN_POOL_TYPES by ABI family, and isLockReleaseTokenPoolType narrows to the lock/release set per getTokenPoolFamily. * feat(cct-sdk): add EVM getTokenPoolState read op (DAPP-10823) Reads a pool's admin state across v1.5.0-v2.0.0 through the getters each version has: one reader per generation, dispatched explicitly rather than floor-matched, so adding a pool version fails to compile instead of silently inheriting a reader. The result is a union discriminated by version, then by type for a lock/release pool's lockBox. v1.5.0 has no getTokenDecimals, so legacy decimals come from the token. * feat(cct-sdk): expose getTokenPoolState on EVMTokenManager (DAPP-10823) Also groups the operation fields by area (token / token admin registry / token pool / lockbox), matching SolanaTokenManager. * refactor(cct-sdk): move SolanaQuery onto the shared Query base (DAPP-10823) GetTokenPoolState gains name + validate and drops its params-conditional result: read now resolves to the union, which removes a query override that only re-typed the base's two steps, along with the two casts that conditional return required. Caller narrowing moves to SolanaTokenManager.getTokenPoolState overloads, so call sites and inferred types are unchanged. * Address PR comments * Address PR comments --- ccip-sdk/src/cct/errors.ts | 19 +- ccip-sdk/src/cct/evm/index.test.ts | 39 ++ ccip-sdk/src/cct/evm/index.ts | 58 ++- ccip-sdk/src/cct/evm/query.ts | 35 ++ .../src/cct/evm/token-pool/contracts.test.ts | 12 + ccip-sdk/src/cct/evm/token-pool/contracts.ts | 11 + .../operations/get-token-pool-state.test.ts | 341 ++++++++++++++++++ .../operations/get-token-pool-state.ts | 286 +++++++++++++++ ccip-sdk/src/cct/query.ts | 28 ++ ccip-sdk/src/cct/solana/index.test.ts | 15 + ccip-sdk/src/cct/solana/index.ts | 43 ++- ccip-sdk/src/cct/solana/query.ts | 19 +- .../operations/get-supported-tokens.ts | 12 +- .../operations/get-token-admin-registry.ts | 25 +- .../operations/get-token-pool-state.test.ts | 2 + .../operations/get-token-pool-state.ts | 59 +-- 16 files changed, 952 insertions(+), 52 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/query.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-state.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-state.ts create mode 100644 ccip-sdk/src/cct/query.ts diff --git a/ccip-sdk/src/cct/errors.ts b/ccip-sdk/src/cct/errors.ts index b09992191..3b9f25fe4 100644 --- a/ccip-sdk/src/cct/errors.ts +++ b/ccip-sdk/src/cct/errors.ts @@ -120,15 +120,26 @@ export class CCTTxNotConfirmedError extends CCIPError { */ export class CCTContractTypeInvalidError extends CCIPError { override readonly name = 'CCTContractTypeInvalidError' - /** Creates a contract-type-invalid error. */ - constructor(address: string, expected: string, actual: string, options?: CCIPErrorOptions) { + /** + * 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}"`, + `Expected a ${expected} contract at ${address}, got "${actual}"` + + (reason ? ` — ${reason}` : ''), { ...options, isTransient: false, - context: { ...options?.context, address, expected, actual }, + context: { ...options?.context, address, expected, actual, ...(reason && { reason }) }, }, ) } diff --git a/ccip-sdk/src/cct/evm/index.test.ts b/ccip-sdk/src/cct/evm/index.test.ts index 4e42e604f..522c4631b 100644 --- a/ccip-sdk/src/cct/evm/index.test.ts +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -197,4 +197,43 @@ describe('EVMTokenManager (cct/evm)', () => { ) }) }) + + 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') + }) + }) }) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index a49a0820e..7b6d45e96 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -26,6 +26,11 @@ 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, @@ -34,10 +39,18 @@ import { /** CCT admin operations for EVM chains, delegating each op to an operation class. */ export class EVMTokenManager extends TokenManager { readonly chain: EVMChain - readonly #setPool = new SetPool() - readonly #transferOwnership = new TransferOwnership() + // Token operations readonly #deployToken = new DeployToken() + + // Token admin registry operations + readonly #setPool = new SetPool() + + // 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() @@ -324,6 +337,39 @@ export class EVMTokenManager extends TokenManager { ): 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' @@ -333,6 +379,14 @@ 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 { diff --git a/ccip-sdk/src/cct/evm/query.ts b/ccip-sdk/src/cct/evm/query.ts new file mode 100644 index 000000000..8406b9eb4 --- /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/token-pool/contracts.test.ts b/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts index 011081726..79ef9d48e 100644 --- a/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts @@ -10,6 +10,7 @@ import { TokenPoolVersion, getTokenPoolFamily, getTokenPoolInterface, + isLockReleaseTokenPoolType, isTokenPoolType, isTokenPoolVersion, parseTokenPoolVersion, @@ -49,6 +50,17 @@ describe('pool types', () => { 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') diff --git a/ccip-sdk/src/cct/evm/token-pool/contracts.ts b/ccip-sdk/src/cct/evm/token-pool/contracts.ts index 511c59bb5..9b9061397 100644 --- a/ccip-sdk/src/cct/evm/token-pool/contracts.ts +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.ts @@ -57,6 +57,12 @@ export const TOKEN_POOL_TYPES = [ /** 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) @@ -73,6 +79,11 @@ 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}. */ 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 000000000..9b9ce2a7f --- /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 000000000..f6f894979 --- /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/query.ts b/ccip-sdk/src/cct/query.ts new file mode 100644 index 000000000..4bf275b0b --- /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 index ab9399e37..5422acd82 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -4,6 +4,10 @@ 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 { @@ -77,4 +81,15 @@ describe('SolanaTokenManager (cct/solana)', () => { 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 index 89aa0cb19..3f59ce703 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -61,6 +61,9 @@ import { TransferAdmin, } from './token-admin-registry/operations/index.ts' import { + type BaseGetTokenPoolStateResult, + type BurnMintPoolProgramRef, + type CustomPoolProgramRef, type ExecuteConfigureAllowlistParams, type ExecuteConfigureAllowlistResult, type ExecuteCreateTokenMultisigParams, @@ -79,6 +82,8 @@ import { type GenerateRemoveFromAllowlistResult, type GetTokenPoolStateParams, type GetTokenPoolStateResult, + type LockReleaseGetTokenPoolStateResult, + type LockReleasePoolProgramRef, ConfigureAllowlist, CreateTokenMultisig, DeployTokenPool, @@ -861,8 +866,14 @@ export class SolanaTokenManager extends TokenManager } /** - * Reads a Burn/Mint, Lock/Release, or custom token pool's state account. - * Pass `poolProgramAddress` instead of `poolType` for a custom pool program. + * 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. @@ -872,14 +883,34 @@ export class SolanaTokenManager extends TokenManager * ```ts * const cct = SolanaTokenManager.fromChain(chain) * const state = await cct.getTokenPoolState({ - * poolType: 'burn-mint', + * 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: P, - ): Promise> { + 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) } diff --git a/ccip-sdk/src/cct/solana/query.ts b/ccip-sdk/src/cct/solana/query.ts index e2d76a879..4ee775701 100644 --- a/ccip-sdk/src/cct/solana/query.ts +++ b/ccip-sdk/src/cct/solana/query.ts @@ -1,6 +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. */ -export abstract class SolanaQuery

{ - abstract query(chain: SolanaChain, params: P): Promise -} +/** 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/token-admin-registry/operations/get-supported-tokens.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-supported-tokens.ts index 2b604eba8..4ae3f6634 100644 --- 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 @@ -13,10 +13,16 @@ export type GetSupportedTokensParams = { /** Lists all SPL token mints configured in a TokenAdminRegistry in a single scan; pagination is not supported. */ export class GetSupportedTokens extends SolanaQuery { - /** Resolves the Router and lists its configured token mints. */ - async query(chain: SolanaChain, params: GetSupportedTokensParams): Promise { - validatePublicKey(this.constructor.name, 'address', params.address) + 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.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/get-token-admin-registry.ts index 66ff12506..463ddbbf9 100644 --- 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 @@ -25,21 +25,32 @@ export type GetTokenAdminRegistryResult = RegistryTokenConfig & { 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 + 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. */ - async query( + protected async read( chain: SolanaChain, - params: GetTokenAdminRegistryParams, + params: ParsedGetTokenAdminRegistryParams, ): Promise { - validatePublicKey(this.constructor.name, 'address', params.address) - const router = new PublicKey(await chain.getTokenAdminRegistryFor(params.address)) - const tokenMint = parsePublicKey(this.constructor.name, 'tokenAddress', params.tokenAddress) - const config = await getTokenAdminRegistryConfig(chain.connection, router, tokenMint) + const config = await getTokenAdminRegistryConfig(chain.connection, router, params.tokenMint) return { mint: config.mint.toBase58(), 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 index 515e2761e..2eff1db3a 100644 --- 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 @@ -64,6 +64,8 @@ describe('GetTokenPoolState (cct/solana)', () => { 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()]) 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 index 93ea6d9ce..3b005aebd 100644 --- 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 @@ -1,3 +1,5 @@ +import type { PublicKey } from '@solana/web3.js' + import { CCIPTokenPoolStateNotFoundError } from '../../../../errors/index.ts' import type { SolanaChain } from '../../../../solana/index.ts' import { @@ -57,24 +59,16 @@ export type LockReleaseGetTokenPoolStateResult = GetTokenPoolStateResultBase & { } } -type TokenPoolStateResultByType = { - 'burn-mint': BaseGetTokenPoolStateResult - 'lock-release': LockReleaseGetTokenPoolStateResult -} - /** * State returned for a canonical or custom token pool program. * - * Results queried with `poolProgramAddress` use the base config shape and omit lock-release-only - * fields, even when the supplied address is the lock-release 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

= P extends { - poolType: infer T -} - ? T extends keyof TokenPoolStateResultByType - ? TokenPoolStateResultByType[T] - : BaseGetTokenPoolStateResult - : BaseGetTokenPoolStateResult +export type GetTokenPoolStateResult = + BaseGetTokenPoolStateResult | LockReleaseGetTokenPoolStateResult function serializeBaseConfig(config: TokenPoolConfig): BaseConfig { return { @@ -94,26 +88,39 @@ function serializeBaseConfig(config: TokenPoolConfig): BaseConfig { } } +/** {@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 + GetTokenPoolStateResult, + ParsedGetTokenPoolStateParams > { - /** Reads and serializes the token pool configuration account. */ - async query

( - chain: SolanaChain, - params: P, - ): Promise> { - return this.fetchPoolState(chain, params) as Promise> + 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), + } } - /** Fetches and serializes the token pool configuration account. */ - private async fetchPoolState( + /** Reads and serializes the token pool config account; the facade's overloads narrow the arm. */ + protected async read( chain: SolanaChain, - params: GetTokenPoolStateParams, + params: ParsedGetTokenPoolStateParams, ): Promise { - const mint = parsePublicKey('getTokenPoolState', 'tokenAddress', params.tokenAddress) - const programId = resolvePoolProgram('getTokenPoolState', params) + const { mint, programId } = params const state = deriveTokenPoolConfigPda(programId, mint) const account = await chain.connection.getAccountInfo(state) From 823f6d1bf6ed2661322881e338fcc57ed4b28d73 Mon Sep 17 00:00:00 2001 From: Mervin Date: Fri, 7 Aug 2026 22:43:12 +0800 Subject: [PATCH 55/87] feat(cct-sdk): Add init chain remote config solana op (#339) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * feat: add accept admin op solana * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments * fix: address comments * fix: address comments * fix: update err.context.reason test * feat: add get token admin registry config op solana * fix: group tests * fix: merge conflicts * fix: remove console logs * fix: refactor test * fix: add tsdoc for decodeTokenAdminRegistryConfig * feat: add get supported tokens op solana * fix: address followup comments * fix: address comments * fix: add validateOptionalPublicKey and refactor test files * fix: update solana lifecycle * fix: remove dev build * feat: add configure allowlist op solana * fix: assert configure allowlist functions * fix: address comments * fix: params.additionalAddresses.length with eslint rule * fix: address comments * feat: add remove from allowlist op solana * fix: add validations and test coverage * fix: add additional tsdoc * feat: add init chain remote config op solana * fix: throw empty buffer * fix: update tsdoc * fix: address comments --- ccip-sdk/src/cct/solana/index.ts | 72 +++++++ .../src/cct/solana/programs/token-pool.ts | 14 ++ .../cct/solana/token-pool/operations/index.ts | 1 + .../init-chain-remote-config.test.ts | 179 ++++++++++++++++++ .../operations/init-chain-remote-config.ts | 178 +++++++++++++++++ ccip-sdk/src/cct/solana/validate.test.ts | 28 +++ ccip-sdk/src/cct/solana/validate.ts | 61 +++++- 7 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/init-chain-remote-config.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/init-chain-remote-config.ts diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 3f59ce703..b2a31c347 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -70,6 +70,8 @@ import { type ExecuteCreateTokenMultisigResult, type ExecuteDeployTokenPoolParams, type ExecuteDeployTokenPoolResult, + type ExecuteInitChainRemoteConfigParams, + type ExecuteInitChainRemoteConfigResult, type ExecuteRemoveFromAllowlistParams, type ExecuteRemoveFromAllowlistResult, type GenerateConfigureAllowlistParams, @@ -78,6 +80,8 @@ import { type GenerateCreateTokenMultisigResult, type GenerateDeployTokenPoolParams, type GenerateDeployTokenPoolResult, + type GenerateInitChainRemoteConfigParams, + type GenerateInitChainRemoteConfigResult, type GenerateRemoveFromAllowlistParams, type GenerateRemoveFromAllowlistResult, type GetTokenPoolStateParams, @@ -88,6 +92,7 @@ import { CreateTokenMultisig, DeployTokenPool, GetTokenPoolState, + InitChainRemoteConfig, RemoveFromAllowlist, } from './token-pool/operations/index.ts' @@ -112,6 +117,7 @@ export class SolanaTokenManager extends TokenManager readonly #createTokenMultisig = new CreateTokenMultisig() readonly #deployTokenPool = new DeployTokenPool() readonly #getTokenPoolState = new GetTokenPoolState() + readonly #initChainRemoteConfig = new InitChainRemoteConfig() readonly #removeFromAllowlist = new RemoveFromAllowlist() /** Creates a Solana CCT manager for an existing chain. */ @@ -489,6 +495,72 @@ export class SolanaTokenManager extends TokenManager return this.#deployTokenPool.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} + * + * @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} + * + * @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 unsigned Solana lookup table extend instructions. * diff --git a/ccip-sdk/src/cct/solana/programs/token-pool.ts b/ccip-sdk/src/cct/solana/programs/token-pool.ts index f1fc4a362..9cad1176b 100644 --- a/ccip-sdk/src/cct/solana/programs/token-pool.ts +++ b/ccip-sdk/src/cct/solana/programs/token-pool.ts @@ -124,6 +124,20 @@ export function deriveTokenPoolSignerPda(poolProgram: PublicKey, mint: PublicKey )[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( diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index 0dd6f2a54..ff7dcf7c8 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -2,4 +2,5 @@ export * from './configure-allowlist.ts' export * from './create-token-multisig.ts' export * from './deploy-token-pool.ts' export * from './get-token-pool-state.ts' +export * from './init-chain-remote-config.ts' export * from './remove-from-allowlist.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 000000000..972465b5b --- /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 000000000..5b56286ca --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/init-chain-remote-config.ts @@ -0,0 +1,178 @@ +import { Buffer } from 'buffer' + +import { type PublicKey, SystemProgram } from '@solana/web3.js' +import BN from 'bn.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, + deriveTokenPoolChainConfigPda, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parseHexBytes, + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, + validateBigInt, + validateInteger, +} from '../../validate.ts' + +const U64_MAX = 0xffff_ffff_ffff_ffffn + +/** 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' + + /** Validation runs in {@link parse}. */ + protected validate(_params: GenerateInitChainRemoteConfigParams): void {} + + /** 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, ...rest } = params + if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + + const generateParams: GenerateInitChainRemoteConfigParams = { + ...rest, + payer: wallet.publicKey.toBase58(), + } + const parsed = this.prepare(generateParams) + 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/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts index 02251b11c..1578fa9f2 100644 --- a/ccip-sdk/src/cct/solana/validate.test.ts +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -4,8 +4,10 @@ import { describe, it } from 'node:test' import { PublicKey } from '@solana/web3.js' import { + parseHexBytes, parsePublicKey, resolvePoolProgram, + validateBigInt, validateInteger, validateNonEmptyString, validateOptionalPublicKey, @@ -23,6 +25,18 @@ describe('Validate (cct/solana)', () => { 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('accepts valid public keys', () => { assert.doesNotThrow(() => validatePublicKey('op', 'payer', PublicKey.default.toBase58())) }) @@ -141,6 +155,20 @@ describe('Validate (cct/solana)', () => { ) }) + 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])) diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index 00be84099..87f3a916d 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -1,3 +1,5 @@ +import { Buffer } from 'buffer' + import { PublicKey } from '@solana/web3.js' import { CCIPAddressInvalidError } from '../../errors/index.ts' @@ -146,11 +148,46 @@ export function validateInteger( const validMax = max === undefined || (validInteger && Number(value) <= max) if (!validInteger || !validMin || !validMax) { - const range = min !== undefined && max !== undefined ? ` between ${min} and ${max}` : '' + 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. @@ -169,3 +206,25 @@ export function validateWritableIndexes( 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') +} From 4cb12a62756c18798a0883bec26124e506813e47 Mon Sep 17 00:00:00 2001 From: Mervin Date: Fri, 7 Aug 2026 23:20:06 +0800 Subject: [PATCH 56/87] feat(cct-sdk): Add edit chain remote config solana op (#340) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * feat: add accept admin op solana * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments * fix: address comments * fix: address comments * fix: update err.context.reason test * feat: add get token admin registry config op solana * fix: group tests * fix: merge conflicts * fix: remove console logs * fix: refactor test * fix: add tsdoc for decodeTokenAdminRegistryConfig * feat: add get supported tokens op solana * fix: address followup comments * fix: address comments * fix: add validateOptionalPublicKey and refactor test files * fix: update solana lifecycle * fix: remove dev build * feat: add configure allowlist op solana * fix: assert configure allowlist functions * fix: address comments * fix: params.additionalAddresses.length with eslint rule * fix: address comments * feat: add remove from allowlist op solana * fix: add validations and test coverage * fix: add additional tsdoc * feat: add init chain remote config op solana * fix: throw empty buffer * fix: update tsdoc * feat: add edit chain remote config op solana * fix: refactor wallet params * fix: address comments * fix: address comments --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 71 +++++++ ccip-sdk/src/cct/solana/operation.ts | 29 +-- .../edit-chain-remote-config.test.ts | 189 ++++++++++++++++++ .../operations/edit-chain-remote-config.ts | 183 +++++++++++++++++ .../cct/solana/token-pool/operations/index.ts | 1 + .../operations/init-chain-remote-config.ts | 11 +- 7 files changed, 464 insertions(+), 22 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/edit-chain-remote-config.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/edit-chain-remote-config.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 5422acd82..c869062fc 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -52,6 +52,8 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(typeof cct.createTokenMultisig, 'function') assert.equal(typeof cct.generateUnsignedDeployTokenPool, 'function') assert.equal(typeof cct.deployTokenPool, '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.getTokenPoolState, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index b2a31c347..f7adeabf6 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -70,6 +70,8 @@ import { type ExecuteCreateTokenMultisigResult, type ExecuteDeployTokenPoolParams, type ExecuteDeployTokenPoolResult, + type ExecuteEditChainRemoteConfigParams, + type ExecuteEditChainRemoteConfigResult, type ExecuteInitChainRemoteConfigParams, type ExecuteInitChainRemoteConfigResult, type ExecuteRemoveFromAllowlistParams, @@ -80,6 +82,8 @@ import { type GenerateCreateTokenMultisigResult, type GenerateDeployTokenPoolParams, type GenerateDeployTokenPoolResult, + type GenerateEditChainRemoteConfigParams, + type GenerateEditChainRemoteConfigResult, type GenerateInitChainRemoteConfigParams, type GenerateInitChainRemoteConfigResult, type GenerateRemoveFromAllowlistParams, @@ -91,6 +95,7 @@ import { ConfigureAllowlist, CreateTokenMultisig, DeployTokenPool, + EditChainRemoteConfig, GetTokenPoolState, InitChainRemoteConfig, RemoveFromAllowlist, @@ -116,6 +121,7 @@ export class SolanaTokenManager extends TokenManager readonly #configureAllowlist = new ConfigureAllowlist() readonly #createTokenMultisig = new CreateTokenMultisig() readonly #deployTokenPool = new DeployTokenPool() + readonly #editChainRemoteConfig = new EditChainRemoteConfig() readonly #getTokenPoolState = new GetTokenPoolState() readonly #initChainRemoteConfig = new InitChainRemoteConfig() readonly #removeFromAllowlist = new RemoveFromAllowlist() @@ -561,6 +567,71 @@ export class SolanaTokenManager extends TokenManager return this.#initChainRemoteConfig.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} + * + * @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} + * + * @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. * diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts index f6a216857..a1f04fa38 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -20,14 +20,6 @@ export type SolanaExecuteParams

= P & { computeUnits?: number } -function withPayer

( - params: SolanaExecuteParams

, - payer: string, -): SolanaGenerateParams

{ - const { wallet: _wallet, computeUnits: _computeUnits, ...rest } = params - return { ...rest, payer } as SolanaGenerateParams

-} - // TODO: migrate remaining Solana operations to parse normalized params. /** * Solana CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. @@ -65,12 +57,23 @@ export abstract class SolanaOperation< return this.buildUnsigned(chain, this.prepare(params)) } - /** Generate, sign, simulate, send, and confirm with wallet.publicKey as payer. */ - async execute(chain: SolanaChain, params: SolanaExecuteParams

): Promise { - const { wallet, computeUnits } = 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 tx = await this.generate(chain, withPayer(params, wallet.publicKey.toBase58())) - return submit(chain, wallet, tx, this.name, computeUnits) + 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/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 000000000..64abd31d6 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/edit-chain-remote-config.test.ts @@ -0,0 +1,189 @@ +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: ['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 000000000..87bb97886 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/edit-chain-remote-config.ts @@ -0,0 +1,183 @@ +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 { + parseHexBytes, + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, + validateBigInt, + validateInteger, +} from '../../validate.ts' + +const U64_MAX = 0xffff_ffff_ffff_ffffn + +/** 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' + + /** Validation runs in {@link parse}. */ + protected validate(_params: GenerateEditChainRemoteConfigParams): void {} + + /** 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) => + parseHexBytes(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/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index ff7dcf7c8..ae7241feb 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -1,6 +1,7 @@ export * from './configure-allowlist.ts' export * from './create-token-multisig.ts' export * from './deploy-token-pool.ts' +export * from './edit-chain-remote-config.ts' export * from './get-token-pool-state.ts' export * from './init-chain-remote-config.ts' export * from './remove-from-allowlist.ts' 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 index 5b56286ca..b0c10a48c 100644 --- 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 @@ -3,10 +3,9 @@ import { Buffer } from 'buffer' import { type PublicKey, SystemProgram } from '@solana/web3.js' import BN from 'bn.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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' import { @@ -156,14 +155,8 @@ export class InitChainRemoteConfig extends SolanaOperation< chain: SolanaChain, params: ExecuteInitChainRemoteConfigParams, ): Promise { - const { wallet, computeUnits, ...rest } = params - if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) - const generateParams: GenerateInitChainRemoteConfigParams = { - ...rest, - payer: wallet.publicKey.toBase58(), - } - const parsed = this.prepare(generateParams) if (params.authority !== undefined) { validateAuthorityMatchesWallet( this.name, From c9616e45ea8023cf877d83f9a3b4bb1fc1e30e7f Mon Sep 17 00:00:00 2001 From: Mervin Date: Fri, 7 Aug 2026 23:49:05 +0800 Subject: [PATCH 57/87] feat(cct-sdk): Add delete chain remote config solana op (#341) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * feat: add accept admin op solana * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments * fix: address comments * fix: address comments * fix: update err.context.reason test * feat: add get token admin registry config op solana * fix: group tests * fix: merge conflicts * fix: remove console logs * fix: refactor test * fix: add tsdoc for decodeTokenAdminRegistryConfig * feat: add get supported tokens op solana * fix: address followup comments * fix: address comments * fix: add validateOptionalPublicKey and refactor test files * fix: update solana lifecycle * fix: remove dev build * feat: add configure allowlist op solana * fix: assert configure allowlist functions * fix: address comments * fix: params.additionalAddresses.length with eslint rule * fix: address comments * feat: add remove from allowlist op solana * fix: add validations and test coverage * fix: add additional tsdoc * feat: add init chain remote config op solana * fix: throw empty buffer * fix: update tsdoc * feat: add edit chain remote config op solana * fix: refactor wallet params * feat: add delete chain remote config op solana * fix: address comments * fix: address comments * fix: address comments --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 81 +++++++++ .../delete-chain-remote-config.test.ts | 158 ++++++++++++++++++ .../operations/delete-chain-remote-config.ts | 133 +++++++++++++++ .../cct/solana/token-pool/operations/index.ts | 1 + 5 files changed, 375 insertions(+) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/delete-chain-remote-config.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/delete-chain-remote-config.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index c869062fc..aae7b4569 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -52,6 +52,8 @@ describe('SolanaTokenManager (cct/solana)', () => { 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.generateUnsignedEditChainRemoteConfig, 'function') assert.equal(typeof cct.editChainRemoteConfig, 'function') assert.equal(typeof cct.generateUnsignedRemoveFromAllowlist, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index f7adeabf6..e697bc217 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -68,6 +68,8 @@ import { type ExecuteConfigureAllowlistResult, type ExecuteCreateTokenMultisigParams, type ExecuteCreateTokenMultisigResult, + type ExecuteDeleteChainRemoteConfigParams, + type ExecuteDeleteChainRemoteConfigResult, type ExecuteDeployTokenPoolParams, type ExecuteDeployTokenPoolResult, type ExecuteEditChainRemoteConfigParams, @@ -80,6 +82,8 @@ import { type GenerateConfigureAllowlistResult, type GenerateCreateTokenMultisigParams, type GenerateCreateTokenMultisigResult, + type GenerateDeleteChainRemoteConfigParams, + type GenerateDeleteChainRemoteConfigResult, type GenerateDeployTokenPoolParams, type GenerateDeployTokenPoolResult, type GenerateEditChainRemoteConfigParams, @@ -94,6 +98,7 @@ import { type LockReleasePoolProgramRef, ConfigureAllowlist, CreateTokenMultisig, + DeleteChainRemoteConfig, DeployTokenPool, EditChainRemoteConfig, GetTokenPoolState, @@ -121,6 +126,7 @@ export class SolanaTokenManager extends TokenManager readonly #configureAllowlist = new ConfigureAllowlist() readonly #createTokenMultisig = new CreateTokenMultisig() readonly #deployTokenPool = new DeployTokenPool() + readonly #deleteChainRemoteConfig = new DeleteChainRemoteConfig() readonly #editChainRemoteConfig = new EditChainRemoteConfig() readonly #getTokenPoolState = new GetTokenPoolState() readonly #initChainRemoteConfig = new InitChainRemoteConfig() @@ -511,6 +517,8 @@ export class SolanaTokenManager extends TokenManager * 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. * @@ -542,6 +550,8 @@ export class SolanaTokenManager extends TokenManager * 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 @@ -567,6 +577,75 @@ export class SolanaTokenManager extends TokenManager 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 replaces an initialized Solana token pool remote-chain * config. Initialize the config first with `generateUnsignedInitChainRemoteConfig`. Each call @@ -575,6 +654,7 @@ export class SolanaTokenManager extends TokenManager * * @see {@link editChainRemoteConfig} * @see {@link generateUnsignedInitChainRemoteConfig} + * @see {@link generateUnsignedDeleteChainRemoteConfig} * * @throws {@link CCTParamsInvalidError} If a pool parameter or remote config value is invalid. * @@ -606,6 +686,7 @@ export class SolanaTokenManager extends TokenManager * * @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 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 000000000..67f13e04d --- /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 000000000..26dcdc7e5 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/delete-chain-remote-config.ts @@ -0,0 +1,133 @@ +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 { + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, + validateBigInt, +} from '../../validate.ts' + +const U64_MAX = 0xffff_ffff_ffff_ffffn + +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' + + /** Validation runs in {@link parse}. */ + protected validate(_params: GenerateDeleteChainRemoteConfigParams): void {} + + /** 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/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index ae7241feb..9a36449f9 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -1,6 +1,7 @@ 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-state.ts' export * from './init-chain-remote-config.ts' From 50d007449fe3f4c4d469adbbc9142bbb1a44bf46 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:22:32 +0100 Subject: [PATCH 58/87] feat(cct-sdk): Add register token evm op (#333) * Better separation of concerns and abstractions * CCT: Add version dispatch + Transfer Ownership * Adjust with base * Address generic error types and doc examples * Fix linting * feat(cct-sdk): Add deploy evm token * Address PR comments * Address PR comments by fixing chain-specific types * feat(cct-sdk): Source chainlink/contracts-ccip artifacts (#303) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * Remove outdated bytecodes * feat(cct-sdk): Add CrossChainToken deployment + token versioning (#305) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * feat(cct-sdk): Add versioned Deploy Token (CCT + ERC20) * Remove outdated bytecodes * Deploy only CrossChainToken * Recover previous type changes * Address preMint specs * feat(cct-sdk): Add deploy evm token pool + type/version resolution * add remarks ts doc comment * Address PR comments * add lockfile to fix ci issue * Add comments * feat(cct-sdk): Add EVM deployLockbox operation (DAPP-10738) Vendor ERC20LockBox v2.0.0 artifacts; add the cached lockbox Interface + DeployLockbox op, wire generateUnsignedDeployLockbox/deployLockbox into EVMTokenManager, and document the LockRelease deploy sequence. * feat(cct-sdk): Add EVM authorizeLockboxCallers operation (DAPP-10788) Add AuthorizeLockboxCallers op (wraps ERC20LockBox applyAuthorizedCallerUpdates) + tests, and wire generateUnsignedAuthorizeLockboxCallers/authorizeLockboxCallers into EVMTokenManager to complete the LockRelease flow. * refactor(cct-sdk): add validateNonZeroAddress + guard single-tx submit * feat(cct-sdk): EVM deploy verification + unify deploy ops * Address PR comments * Review pass * feat(cct-sdk): add cross-family Query read base (DAPP-10823) Query wires validate -> read so params are rejected before any RPC, mirroring how Operation.generate gates buildUnsigned on the write side. EVMQuery binds it to an EVMChain and owns getTypedContract, the single ethers -> ethers-abitype bridge that CCT read ops decode through. * feat(cct-sdk): add pool family type guards (DAPP-10823) BurnMintTokenPoolType / LockReleaseTokenPoolType split TOKEN_POOL_TYPES by ABI family, and isLockReleaseTokenPoolType narrows to the lock/release set per getTokenPoolFamily. * feat(cct-sdk): add EVM getTokenPoolState read op (DAPP-10823) Reads a pool's admin state across v1.5.0-v2.0.0 through the getters each version has: one reader per generation, dispatched explicitly rather than floor-matched, so adding a pool version fails to compile instead of silently inheriting a reader. The result is a union discriminated by version, then by type for a lock/release pool's lockBox. v1.5.0 has no getTokenDecimals, so legacy decimals come from the token. * feat(cct-sdk): expose getTokenPoolState on EVMTokenManager (DAPP-10823) Also groups the operation fields by area (token / token admin registry / token pool / lockbox), matching SolanaTokenManager. * refactor(cct-sdk): move SolanaQuery onto the shared Query base (DAPP-10823) GetTokenPoolState gains name + validate and drops its params-conditional result: read now resolves to the union, which removes a query override that only re-typed the base's two steps, along with the two casts that conditional return required. Caller narrowing moves to SolanaTokenManager.getTokenPoolState overloads, so call sites and inferred types are unchanged. * Address PR comments * feat(cct-sdk): Add register token evm op --------- Co-authored-by: mervin-link --- .../V1_5_0/registry-module-owner-custom.ts | 68 ++ .../abi/V1_5_0/token-admin-registry.ts | 335 +++++++++ .../V1_6_0/registry-module-owner-custom.ts | 84 +++ ccip-sdk/src/cct/evm/index.test.ts | 153 +++- ccip-sdk/src/cct/evm/index.ts | 72 ++ ccip-sdk/src/cct/evm/operation.ts | 35 +- .../cct/evm/token-admin-registry/contracts.ts | 185 +++++ .../operations/register-admin.test.ts | 656 ++++++++++++++++++ .../operations/register-admin.ts | 259 +++++++ .../operations/set-pool.ts | 4 +- 10 files changed, 1842 insertions(+), 9 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/registry-module-owner-custom.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V1_5_0/token-admin-registry.ts create mode 100644 ccip-sdk/src/cct/evm/artifacts/abi/V1_6_0/registry-module-owner-custom.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/contracts.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/register-admin.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/register-admin.ts 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 000000000..33745a3be --- /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 000000000..1e2ddcce9 --- /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_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 000000000..ef36c1115 --- /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/index.test.ts b/ccip-sdk/src/cct/evm/index.test.ts index 522c4631b..543669a2f 100644 --- a/ccip-sdk/src/cct/evm/index.test.ts +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -1,10 +1,11 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' -import { Interface, id } from 'ethers' +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' @@ -13,6 +14,8 @@ 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) /** Minimal EVMChain stub — only the members EVMTokenManager touches. */ function stubChain(overrides: Partial = {}, poolVersion = '1.5.1'): EVMChain { @@ -20,7 +23,12 @@ function stubChain(overrides: Partial = {}, poolVersion = '1.5.1'): EV provider: {} as never, logger: { debug() {}, info() {}, warn() {}, error() {} }, getTokenAdminRegistryFor: (_address: string) => Promise.resolve(TAR), - typeAndVersion: (_address: string) => Promise.resolve(['BurnMintTokenPool', poolVersion]), + typeAndVersion: (address: string) => + Promise.resolve( + address === REGISTRY_MODULE + ? ['RegistryModuleOwnerCustom', '1.6.0'] + : ['BurnMintTokenPool', poolVersion], + ), nextNonce: async () => 0, rollbackNonce: () => {}, ...overrides, @@ -30,16 +38,49 @@ function stubChain(overrides: Partial = {}, poolVersion = '1.5.1'): EV const HASH = '0x' + 'ab'.repeat(32) /** Fake ethers Signer whose broadcast resolves to a confirmed receipt. */ -function fakeSigner() { +function fakeSigner(address = TOKEN) { return { signTransaction: () => Promise.resolve('0x'), - getAddress: () => Promise.resolve(TOKEN), + 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 EXPECTED_DATA = new Interface([ 'function setPool(address localToken, address pool)', @@ -59,6 +100,110 @@ describe('EVMTokenManager (cct/evm)', () => { }) }) + 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()) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 7b6d45e96..59f0da449 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -21,6 +21,10 @@ import { 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 RegisterAdminParams, + RegisterAdmin, +} from './token-admin-registry/operations/register-admin.ts' import { type SetPoolParams, SetPool } from './token-admin-registry/operations/set-pool.ts' import { type DeployTokenPoolParams, @@ -43,6 +47,7 @@ export class EVMTokenManager extends TokenManager { readonly #deployToken = new DeployToken() // Token admin registry operations + readonly #registerAdmin = new RegisterAdmin() readonly #setPool = new SetPool() // Token pool operations @@ -83,6 +88,69 @@ export class EVMTokenManager extends TokenManager { 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. @@ -373,6 +441,10 @@ export class EVMTokenManager extends TokenManager { } export * from '../errors.ts' +export type { + RegisterAdminMethod, + RegisterAdminParams, +} from './token-admin-registry/operations/register-admin.ts' export type { SetPoolParams } from './token-admin-registry/operations/set-pool.ts' export type { DeployTokenParams } from './token/operations/deploy-token.ts' export type { diff --git a/ccip-sdk/src/cct/evm/operation.ts b/ccip-sdk/src/cct/evm/operation.ts index 4dc0b157c..8928da204 100644 --- a/ccip-sdk/src/cct/evm/operation.ts +++ b/ccip-sdk/src/cct/evm/operation.ts @@ -9,12 +9,13 @@ * @packageDocumentation */ -import type { Interface } from 'ethers' +import { type Interface, getAddress } from 'ethers' -import type { EVMChain } from '../../evm/index.ts' +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 { CCTTxFailedError } from '../errors.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' @@ -103,6 +104,34 @@ export abstract class EVMOperation

extends Operat 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( 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 000000000..8893d5c89 --- /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/register-admin.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/register-admin.test.ts new file mode 100644 index 000000000..7c124173f --- /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 000000000..e454efbc6 --- /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.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.ts index ed48c3ab5..657607036 100644 --- 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 @@ -5,11 +5,11 @@ * @packageDocumentation */ -import { interfaces } from '../../../../evm/const.ts' 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 = { @@ -40,7 +40,7 @@ export class SetPool extends EVMOperation { 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 = interfaces.TokenAdminRegistry.encodeFunctionData('setPool', [ + const data = getTokenAdminRegistryInterface().encodeFunctionData('setPool', [ p.tokenAddress, p.poolAddress, ]) From 6723474ec73ee1e9cf453abfe8dd29c4b52b81ea Mon Sep 17 00:00:00 2001 From: Mervin Date: Mon, 10 Aug 2026 11:17:35 +0800 Subject: [PATCH 59/87] fix(cct-sdk): Migrate solana ops to use parse (#342) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * feat: add accept admin op solana * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments * fix: address comments * fix: address comments * fix: update err.context.reason test * feat: add get token admin registry config op solana * fix: group tests * fix: merge conflicts * fix: remove console logs * fix: refactor test * fix: add tsdoc for decodeTokenAdminRegistryConfig * feat: add get supported tokens op solana * fix: address followup comments * fix: address comments * fix: add validateOptionalPublicKey and refactor test files * fix: update solana lifecycle * fix: remove dev build * feat: add configure allowlist op solana * fix: assert configure allowlist functions * fix: address comments * fix: params.additionalAddresses.length with eslint rule * fix: address comments * feat: add remove from allowlist op solana * fix: add validations and test coverage * fix: add additional tsdoc * feat: add init chain remote config op solana * fix: throw empty buffer * fix: update tsdoc * feat: add edit chain remote config op solana * fix: refactor wallet params * feat: add delete chain remote config op solana * fix: migrate solana ops to use parse * fix: update tsdoc --- ccip-sdk/src/cct/solana/operation.test.ts | 4 ++-- ccip-sdk/src/cct/solana/operation.ts | 15 +++++++++++---- .../operations/accept-admin.ts | 3 --- .../operations/append-to-lookup-table.ts | 9 ++++++--- .../operations/create-lookup-table.ts | 11 +++++++---- .../operations/register-admin.ts | 7 ++++--- .../token-admin-registry/operations/set-pool.ts | 5 +++-- .../operations/transfer-admin.ts | 7 ++++--- .../token-pool/operations/configure-allowlist.ts | 3 --- .../operations/create-token-multisig.ts | 9 ++++++--- .../operations/delete-chain-remote-config.ts | 3 --- .../token-pool/operations/deploy-token-pool.ts | 7 ++++--- .../operations/edit-chain-remote-config.ts | 3 --- .../operations/init-chain-remote-config.ts | 3 --- .../operations/remove-from-allowlist.ts | 3 --- .../token/operations/create-token-account.ts | 7 +++++-- .../cct/solana/token/operations/deploy-token.ts | 5 +++-- 17 files changed, 55 insertions(+), 49 deletions(-) diff --git a/ccip-sdk/src/cct/solana/operation.test.ts b/ccip-sdk/src/cct/solana/operation.test.ts index 54e45c452..65d812d6d 100644 --- a/ccip-sdk/src/cct/solana/operation.test.ts +++ b/ccip-sdk/src/cct/solana/operation.test.ts @@ -14,7 +14,7 @@ class TestOperation extends SolanaOperation<{ value: string }> { captured?: string validated?: string - protected validate(params: { payer: string }): void { + protected override validate(params: { payer: string }): void { this.validated = params.payer } @@ -36,7 +36,7 @@ class ParsedTestOperation extends SolanaOperation< readonly lifecycle: string[] = [] captured?: { payer: string; value: number } - protected validate(params: { payer: string; value: string }): void { + protected override validate(params: { payer: string; value: string }): void { this.lifecycle.push(`validate:${params.value}`) } diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts index a1f04fa38..03272cb18 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -22,17 +22,24 @@ export type SolanaExecuteParams

= P & { // TODO: migrate remaining Solana operations to parse normalized params. /** - * Solana CCT write base. Subclasses supply {@link validate} and {@link buildUnsigned}. + * Solana CCT write base. Subclasses supply {@link parse} and {@link buildUnsigned}. * - * Use {@link validate} for cross-field constraints. Override {@link parse} for per-field - * validation, defaults, or conversion; it must be overridden whenever `Parsed` differs from - * `SolanaGenerateParams

`. + * 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. * 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 index f7a64e841..bf2828f77 100644 --- 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 @@ -58,9 +58,6 @@ export class AcceptAdmin extends SolanaOperation< > { readonly name = 'acceptAdmin' - /** No cross-field constraints; {@link parse} validates individual public-key parameters. */ - protected validate(_params: GenerateAcceptAdminParams): void {} - /** 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) 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 index d2c6decad..baea1e28f 100644 --- 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 @@ -69,8 +69,10 @@ export class AppendToLookupTable extends SolanaOperation< > { readonly name = 'appendToLookupTable' - /** Validates all public keys before any RPC. */ - protected validate(params: GenerateAppendToLookupTableParams): void { + /** Parses all public keys before any RPC. */ + protected override parse( + params: GenerateAppendToLookupTableParams, + ): GenerateAppendToLookupTableParams { validatePublicKey(this.name, 'lookupTableAddress', params.lookupTableAddress) validatePublicKey(this.name, 'payer', params.payer) validateOptionalPublicKey(this.name, 'authority', params.authority) @@ -99,6 +101,7 @@ export class AppendToLookupTable extends SolanaOperation< 'must provide tokenAddress/poolProgramAddress or additionalAddresses', ) } + return params } /** Builds unsigned ALT extend instructions. */ @@ -194,7 +197,7 @@ export class AppendToLookupTable extends SolanaOperation< const payer = wallet.publicKey.toBase58() const generateParams: GenerateAppendToLookupTableParams = { ...rest, payer } - this.validate(generateParams) + this.parse(generateParams) const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined if (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 index 567dfd35d..666996479 100644 --- 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 @@ -67,17 +67,20 @@ export class CreateLookupTable extends SolanaOperation< > { readonly name = 'createLookupTable' - /** Validates params before `buildUnsigned()` performs any RPC. */ - protected validate(params: GenerateCreateLookupTableParams): void { + /** Parses params before `buildUnsigned()` performs any RPC. */ + protected override parse( + params: GenerateCreateLookupTableParams, + ): GenerateCreateLookupTableParams { validatePublicKey(this.name, 'payer', params.payer) validateOptionalPublicKey(this.name, 'authority', params.authority) - if (params.mode === 'createEmpty') return + if (params.mode === 'createEmpty') return params validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) resolvePoolProgram(this.name, params) for (const [i, address] of (params.additionalAddresses ?? []).entries()) { validatePublicKey(this.name, `additionalAddresses[${i}]`, address) } + return params } /** Builds unsigned ALT create instructions, optionally with extend instructions. */ @@ -165,7 +168,7 @@ export class CreateLookupTable extends SolanaOperation< const payer = wallet.publicKey.toBase58() const generateParams: GenerateCreateLookupTableParams = { ...rest, payer } - this.validate(generateParams) + this.parse(generateParams) const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined if (params.mode !== 'createEmpty' && 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 index e3276fbe8..e90063243 100644 --- 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 @@ -134,8 +134,8 @@ async function buildCcipAdminInstruction( export class RegisterAdmin extends SolanaOperation { readonly name = 'registerAdmin' - /** Validates all caller-supplied parameters before RPC. */ - protected validate(params: GenerateRegisterAdminParams): void { + /** Parses all caller-supplied parameters before RPC. */ + protected override parse(params: GenerateRegisterAdminParams): GenerateRegisterAdminParams { validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) validatePublicKey(this.name, 'address', params.address) validatePublicKey(this.name, 'payer', params.payer) @@ -151,6 +151,7 @@ export class RegisterAdmin extends SolanaOperation { 'must be owner or ccip-admin', ) } + return params } /** Builds an unsigned token registration instruction. */ @@ -227,7 +228,7 @@ export class RegisterAdmin extends SolanaOperation { const payer = wallet.publicKey.toBase58() const generateParams: GenerateRegisterAdminParams = { ...rest, payer } - this.validate(generateParams) + this.parse(generateParams) const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined if (authority) { 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 index 4233c0eb5..e6d777d6f 100644 --- 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 @@ -65,14 +65,15 @@ export type ExecuteSetPoolResult = TransactionResult export class SetPool extends SolanaOperation { readonly name = 'setPool' - /** Validates all public keys before any RPC. */ - protected validate(params: GenerateSetPoolParams): void { + /** Parses all public keys before any RPC. */ + protected override parse(params: GenerateSetPoolParams): GenerateSetPoolParams { validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) validatePublicKey(this.name, 'address', params.address) validatePublicKey(this.name, 'poolLookupTableAddress', params.poolLookupTableAddress) validatePublicKey(this.name, 'payer', params.payer) validateOptionalPublicKey(this.name, 'authority', params.authority) validateWritableIndexes(this.name, 'writableIndexes', params.writableIndexes) + return params } /** Builds the unsigned Solana `setPool` instruction set. */ 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 index eb710a349..2438c4b37 100644 --- 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 @@ -53,13 +53,14 @@ export type ExecuteTransferAdminResult = TransactionResult export class TransferAdmin extends SolanaOperation { readonly name = 'transferAdmin' - /** Validates all public keys before any RPC. */ - protected validate(params: GenerateTransferAdminParams): void { + /** Parses all public keys before any RPC. */ + protected override parse(params: GenerateTransferAdminParams): GenerateTransferAdminParams { validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) validatePublicKey(this.name, 'address', params.address) validatePublicKey(this.name, 'newAdmin', params.newAdmin) validatePublicKey(this.name, 'payer', params.payer) validateOptionalPublicKey(this.name, 'authority', params.authority) + return params } /** Builds the unsigned instruction after confirming the caller is the current admin. */ @@ -111,7 +112,7 @@ export class TransferAdmin extends SolanaOperation { const payer = wallet.publicKey.toBase58() const generateParams: GenerateTransferAdminParams = { ...rest, payer } - this.validate(generateParams) + this.parse(generateParams) if (params.authority !== undefined) { validateAuthorityMatchesWallet( 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 index f935546bd..fba26abf1 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.ts @@ -64,9 +64,6 @@ export class ConfigureAllowlist extends SolanaOperation< > { readonly name = 'configureAllowlist' - /** {@link parse} validates and normalizes all parameters. */ - protected validate(_params: GenerateConfigureAllowlistParams): void {} - /** Parses public keys and defaults authority to payer without mutating caller params. */ protected override parse( params: GenerateConfigureAllowlistParams, 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 index 6b52b2ff0..2a43a95c0 100644 --- 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 @@ -133,8 +133,10 @@ export class CreateTokenMultisig extends SolanaOperation< > { readonly name = 'createTokenMultisig' - /** Validates public keys, threshold, and optional seed before mint/account RPCs. */ - protected validate(params: GenerateCreateTokenMultisigParams): void { + /** Parses public keys, threshold, and optional seed before mint/account RPCs. */ + protected override parse( + params: GenerateCreateTokenMultisigParams, + ): GenerateCreateTokenMultisigParams { validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) validatePoolType(this.name, 'poolType', params.poolType) validatePublicKey(this.name, 'payer', params.payer) @@ -143,6 +145,7 @@ export class CreateTokenMultisig extends SolanaOperation< } validateInteger(this.name, 'threshold', params.threshold, 1, SOLANA_MULTISIG_MAX_SIGNERS) if (params.seed !== undefined) validateNonEmptyString(this.name, 'seed', params.seed) + return params } /** Builds create-with-seed and initialize-multisig instructions. */ @@ -213,7 +216,7 @@ export class CreateTokenMultisig extends SolanaOperation< ...rest, payer: wallet.publicKey.toBase58(), } - this.validate(generateParams) + this.parse(generateParams) const tokenMint = new PublicKey(generateParams.tokenAddress) const mintAccount = await resolveTokenMint(chain.connection, tokenMint) 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 index 26dcdc7e5..40d1e528b 100644 --- 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 @@ -65,9 +65,6 @@ export class DeleteChainRemoteConfig extends SolanaOperation< > { readonly name = 'deleteChainRemoteConfig' - /** Validation runs in {@link parse}. */ - protected validate(_params: GenerateDeleteChainRemoteConfigParams): void {} - /** Parses addresses and defaults authority to payer without mutating caller params. */ protected override parse( params: GenerateDeleteChainRemoteConfigParams, 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 index 1712fb47f..80ed2f036 100644 --- 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 @@ -77,13 +77,14 @@ export class DeployTokenPool extends SolanaOperation< > { readonly name = 'deployTokenPool' - /** Validates all public keys before any RPC. */ - protected validate(params: GenerateDeployTokenPoolParams): void { + /** Parses all public keys before any RPC. */ + protected override parse(params: GenerateDeployTokenPoolParams): GenerateDeployTokenPoolParams { validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) validatePoolType(this.name, 'poolType', params.poolType) validatePublicKey(this.name, 'payer', params.payer) validateOptionalPublicKey(this.name, 'authority', params.authority) if (params.allowlist !== undefined) validatePublicKeys(this.name, 'allowlist', params.allowlist) + return params } /** Builds the unsigned Solana token pool initialize instruction set. */ @@ -151,7 +152,7 @@ export class DeployTokenPool extends SolanaOperation< const payer = wallet.publicKey.toBase58() const generateParams: GenerateDeployTokenPoolParams = { ...rest, payer } - this.validate(generateParams) + this.parse(generateParams) const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined if (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 index 87bb97886..4f5d24616 100644 --- 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 @@ -87,9 +87,6 @@ export class EditChainRemoteConfig extends SolanaOperation< > { readonly name = 'editChainRemoteConfig' - /** Validation runs in {@link parse}. */ - protected validate(_params: GenerateEditChainRemoteConfigParams): void {} - /** Parses config values and defaults authority to payer without mutating caller params. */ protected override parse( params: GenerateEditChainRemoteConfigParams, 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 index b0c10a48c..fc1658753 100644 --- 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 @@ -79,9 +79,6 @@ export class InitChainRemoteConfig extends SolanaOperation< > { readonly name = 'initChainRemoteConfig' - /** Validation runs in {@link parse}. */ - protected validate(_params: GenerateInitChainRemoteConfigParams): void {} - /** Parses config values and defaults authority to payer without mutating caller params. */ protected override parse( params: GenerateInitChainRemoteConfigParams, 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 index a227648e6..42a4f2dea 100644 --- 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 @@ -65,9 +65,6 @@ export class RemoveFromAllowlist extends SolanaOperation< > { readonly name = 'removeFromAllowlist' - /** Validation runs in {@link parse}. */ - protected validate(_params: GenerateRemoveFromAllowlistParams): void {} - /** Parses public keys and defaults authority to payer without mutating caller params. */ protected override parse( params: GenerateRemoveFromAllowlistParams, 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 index 94a8defc2..79755b6be 100644 --- a/ccip-sdk/src/cct/solana/token/operations/create-token-account.ts +++ b/ccip-sdk/src/cct/solana/token/operations/create-token-account.ts @@ -42,11 +42,14 @@ export class CreateTokenAccount extends SolanaOperation< > { readonly name = 'createTokenAccount' - /** Validates create-token-account parameters. */ - protected validate(params: GenerateCreateTokenAccountParams): void { + /** Parses create-token-account parameters. */ + protected override parse( + params: GenerateCreateTokenAccountParams, + ): GenerateCreateTokenAccountParams { validatePublicKey(this.name, 'payer', params.payer) validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) validatePublicKey(this.name, 'ownerAddress', params.ownerAddress) + return params } /** Builds an unsigned idempotent associated token account creation transaction. */ diff --git a/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts index 5c6868aad..baf9fbf6e 100644 --- a/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts @@ -296,11 +296,12 @@ function validateMetaplexParams(operation: string, params: GenerateDeployTokenPa export class DeployToken extends SolanaOperation { readonly name = 'deployToken' - /** Validates mint and metadata params before any RPC. */ - protected validate(params: GenerateDeployTokenParams): void { + /** Parses mint and metadata params before any RPC. */ + protected override parse(params: GenerateDeployTokenParams): GenerateDeployTokenParams { validateBaseParams(this.name, params) validatePreMintParams(this.name, params) validateMetaplexParams(this.name, params) + return params } /** Builds the unsigned Solana mint creation instruction set. */ From 6ee85ddb44777d0244390a77e1ef15f585b03e2a Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Mon, 10 Aug 2026 07:01:09 +0100 Subject: [PATCH 60/87] feat(cct-sdk): Add transfer admin evm op (#334) * Better separation of concerns and abstractions * CCT: Add version dispatch + Transfer Ownership * Adjust with base * Address generic error types and doc examples * Fix linting * feat(cct-sdk): Add deploy evm token * Address PR comments * Address PR comments by fixing chain-specific types * feat(cct-sdk): Source chainlink/contracts-ccip artifacts (#303) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * Remove outdated bytecodes * feat(cct-sdk): Add CrossChainToken deployment + token versioning (#305) * feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip * feat(cct-sdk): Add versioned Deploy Token (CCT + ERC20) * Remove outdated bytecodes * Deploy only CrossChainToken * Recover previous type changes * Address preMint specs * feat(cct-sdk): Add deploy evm token pool + type/version resolution * add remarks ts doc comment * Address PR comments * add lockfile to fix ci issue * Add comments * feat(cct-sdk): Add EVM deployLockbox operation (DAPP-10738) Vendor ERC20LockBox v2.0.0 artifacts; add the cached lockbox Interface + DeployLockbox op, wire generateUnsignedDeployLockbox/deployLockbox into EVMTokenManager, and document the LockRelease deploy sequence. * feat(cct-sdk): Add EVM authorizeLockboxCallers operation (DAPP-10788) Add AuthorizeLockboxCallers op (wraps ERC20LockBox applyAuthorizedCallerUpdates) + tests, and wire generateUnsignedAuthorizeLockboxCallers/authorizeLockboxCallers into EVMTokenManager to complete the LockRelease flow. * refactor(cct-sdk): add validateNonZeroAddress + guard single-tx submit * feat(cct-sdk): EVM deploy verification + unify deploy ops * Address PR comments * Review pass * feat(cct-sdk): add cross-family Query read base (DAPP-10823) Query wires validate -> read so params are rejected before any RPC, mirroring how Operation.generate gates buildUnsigned on the write side. EVMQuery binds it to an EVMChain and owns getTypedContract, the single ethers -> ethers-abitype bridge that CCT read ops decode through. * feat(cct-sdk): add pool family type guards (DAPP-10823) BurnMintTokenPoolType / LockReleaseTokenPoolType split TOKEN_POOL_TYPES by ABI family, and isLockReleaseTokenPoolType narrows to the lock/release set per getTokenPoolFamily. * feat(cct-sdk): add EVM getTokenPoolState read op (DAPP-10823) Reads a pool's admin state across v1.5.0-v2.0.0 through the getters each version has: one reader per generation, dispatched explicitly rather than floor-matched, so adding a pool version fails to compile instead of silently inheriting a reader. The result is a union discriminated by version, then by type for a lock/release pool's lockBox. v1.5.0 has no getTokenDecimals, so legacy decimals come from the token. * feat(cct-sdk): expose getTokenPoolState on EVMTokenManager (DAPP-10823) Also groups the operation fields by area (token / token admin registry / token pool / lockbox), matching SolanaTokenManager. * refactor(cct-sdk): move SolanaQuery onto the shared Query base (DAPP-10823) GetTokenPoolState gains name + validate and drops its params-conditional result: read now resolves to the union, which removes a query override that only re-typed the base's two steps, along with the two casts that conditional return required. Caller narrowing moves to SolanaTokenManager.getTokenPoolState overloads, so call sites and inferred types are unchanged. * Address PR comments * feat(cct-sdk): Add register token evm op * feat(cct-sdk): Add transfer admin evm op * docs(cct-sdk): document ZeroAddress cancellation behavior and add role distinction warning to transferAdmin execute method * feat(cct-sdk): Add accept admin evm op (#328) * feat(cct-sdk): Add accept admin evm op * feat(cct-sdk): Add get token admin registry config evm query (#335) * feat(cct-sdk): Add get token admin registry config evm op * docs(cct-sdk): add ZeroAddress caveat to GetTokenAdminRegistryResult type alias Clarify that administrator may be ZeroAddress for pending-acceptance state; guide callers to test with === ZeroAddress instead of truthiness to avoid silently missing the pending-registration case. * feat(cct-sdk): Add get supported tokens evm query (#336) * feat(cct-sdk): Add get supported tokens evm query * docs+types(cct-sdk): add default page size and explicit result type to getSupportedTokens - Document that page parameter defaults to 1000 tokens per call - Add explicit GetSupportedTokensResult type export for surface parity with sibling ops and Solana variant - Makes the intent clearer and provides consistency across CCT query operations * fix: use GetSupportedTokensResult --------- Co-authored-by: aelmanaa Co-authored-by: mervin-link --------- Co-authored-by: aelmanaa Co-authored-by: mervin-link --------- Co-authored-by: aelmanaa Co-authored-by: mervin-link --------- Co-authored-by: aelmanaa Co-authored-by: mervin-link --- ccip-sdk/src/cct/evm/index.test.ts | 343 ++++++++++++++++- ccip-sdk/src/cct/evm/index.ts | 164 ++++++++ .../operations/accept-admin.test.ts | 357 ++++++++++++++++++ .../operations/accept-admin.ts | 114 ++++++ .../operations/get-supported-tokens.test.ts | 126 +++++++ .../operations/get-supported-tokens.ts | 68 ++++ .../get-token-admin-registry.test.ts | 181 +++++++++ .../operations/get-token-admin-registry.ts | 84 +++++ .../operations/transfer-admin.test.ts | 301 +++++++++++++++ .../operations/transfer-admin.ts | 139 +++++++ 10 files changed, 1876 insertions(+), 1 deletion(-) create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/accept-admin.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/get-supported-tokens.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/get-supported-tokens.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/get-token-admin-registry.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/get-token-admin-registry.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin.ts diff --git a/ccip-sdk/src/cct/evm/index.test.ts b/ccip-sdk/src/cct/evm/index.test.ts index 543669a2f..5df1639bd 100644 --- a/ccip-sdk/src/cct/evm/index.test.ts +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -16,11 +16,22 @@ 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: {} as never, + provider: { call: async () => encodeTokenConfig(CURRENT_ADMIN) }, logger: { debug() {}, info() {}, warn() {}, error() {} }, getTokenAdminRegistryFor: (_address: string) => Promise.resolve(TAR), typeAndVersion: (address: string) => @@ -82,12 +93,32 @@ function registerAdminProvider() { } 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', () => { @@ -305,6 +336,76 @@ describe('EVMTokenManager (cct/evm)', () => { }) }) + 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[] = [] @@ -381,4 +482,244 @@ describe('EVMTokenManager (cct/evm)', () => { 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 index 59f0da449..b0d41bc40 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -21,11 +21,29 @@ import { 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, @@ -49,6 +67,10 @@ export class EVMTokenManager extends TokenManager { // 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() @@ -191,6 +213,138 @@ export class EVMTokenManager extends TokenManager { 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` @@ -441,11 +595,21 @@ export class EVMTokenManager extends TokenManager { } 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, 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 000000000..3b6bf188f --- /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 000000000..e83ba6cc5 --- /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 000000000..0f6150cb9 --- /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 000000000..00d2418b9 --- /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 000000000..0258a3ca9 --- /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 000000000..06344480a --- /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/transfer-admin.test.ts b/ccip-sdk/src/cct/evm/token-admin-registry/operations/transfer-admin.test.ts new file mode 100644 index 000000000..a5f18af5a --- /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 000000000..a09f3b1a6 --- /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 }) + } +} From 14a39fcb522f6e97af7df82dab2da05ee0598455 Mon Sep 17 00:00:00 2001 From: Mervin Date: Mon, 10 Aug 2026 22:39:27 +0800 Subject: [PATCH 61/87] feat(cct-sdk): Add set chain rate limit op solana (#344) * feat: add deploy token op solana * feat: add deploy token pool op solana * fix: add wallet and authority assertion * fix: add tsdoc * fix: add tsdoc * feat: add create token account op solana * fix: group ops in SolanaTokenManager * fix: address comments * fix: remove helper function and override execute * fix: address comments * fix: address comments * fix: refactor validators * fix: export pool programs and type * fix: add validateAuthorityMatchesWallet * fix: use validateAuthorityMatchesWallet * fix: add @remarks to deploy token pool * feat: add get token pool state op solana * fix: address comments * fix: let decodeTokenPoolState catch errors * fix: add parsePublicKey helper * fix: move getTokenPoolState declaration * fix: address comments * fix: export resolveTokenMint * feat: add register token op solana * fix: update register token tests * feat: add propose admin op solana * fix: get pool state tests * feat: add accept admin op solana * fix: address comments * fix: update tsdoc for register admin * fix: update tsdoc for register admin * fix: address comments * fix: address comments * fix: address comments * fix: update err.context.reason test * feat: add get token admin registry config op solana * fix: group tests * fix: merge conflicts * fix: remove console logs * fix: refactor test * fix: add tsdoc for decodeTokenAdminRegistryConfig * feat: add get supported tokens op solana * fix: address followup comments * fix: address comments * fix: add validateOptionalPublicKey and refactor test files * fix: update solana lifecycle * fix: remove dev build * feat: add configure allowlist op solana * fix: assert configure allowlist functions * fix: address comments * fix: params.additionalAddresses.length with eslint rule * fix: address comments * feat: add remove from allowlist op solana * fix: add validations and test coverage * fix: add additional tsdoc * feat: add init chain remote config op solana * fix: throw empty buffer * fix: update tsdoc * feat: add edit chain remote config op solana * fix: refactor wallet params * feat: add delete chain remote config op solana * fix: migrate solana ops to use parse * feat: add set rate limit op solana * fix: defaults to zeroes if enabled is false * fix: update tsdoc * feat(cct-sdk): Add set chain rate limit admin op solana (#347) feat: add set rate limit admin op solana --- ccip-sdk/src/cct/solana/index.test.ts | 4 + ccip-sdk/src/cct/solana/index.ts | 142 +++++++++++ .../cct/solana/token-pool/operations/index.ts | 2 + .../operations/set-chain-rate-limit.test.ts | 228 ++++++++++++++++++ .../operations/set-chain-rate-limit.ts | 226 +++++++++++++++++ .../operations/set-rate-limit-admin.test.ts | 146 +++++++++++ .../operations/set-rate-limit-admin.ts | 115 +++++++++ 7 files changed, 863 insertions(+) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/set-chain-rate-limit.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/set-chain-rate-limit.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/set-rate-limit-admin.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/set-rate-limit-admin.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index aae7b4569..f35ff0996 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -54,6 +54,10 @@ describe('SolanaTokenManager (cct/solana)', () => { 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.generateUnsignedEditChainRemoteConfig, 'function') assert.equal(typeof cct.editChainRemoteConfig, 'function') assert.equal(typeof cct.generateUnsignedRemoveFromAllowlist, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index e697bc217..1a661b240 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -78,6 +78,10 @@ import { type ExecuteInitChainRemoteConfigResult, type ExecuteRemoveFromAllowlistParams, type ExecuteRemoveFromAllowlistResult, + type ExecuteSetChainRateLimitParams, + type ExecuteSetChainRateLimitResult, + type ExecuteSetRateLimitAdminParams, + type ExecuteSetRateLimitAdminResult, type GenerateConfigureAllowlistParams, type GenerateConfigureAllowlistResult, type GenerateCreateTokenMultisigParams, @@ -92,6 +96,10 @@ import { type GenerateInitChainRemoteConfigResult, type GenerateRemoveFromAllowlistParams, type GenerateRemoveFromAllowlistResult, + type GenerateSetChainRateLimitParams, + type GenerateSetChainRateLimitResult, + type GenerateSetRateLimitAdminParams, + type GenerateSetRateLimitAdminResult, type GetTokenPoolStateParams, type GetTokenPoolStateResult, type LockReleaseGetTokenPoolStateResult, @@ -104,6 +112,8 @@ import { GetTokenPoolState, InitChainRemoteConfig, RemoveFromAllowlist, + SetChainRateLimit, + SetRateLimitAdmin, } from './token-pool/operations/index.ts' /** CCT admin facade for Solana. */ @@ -131,6 +141,8 @@ export class SolanaTokenManager extends TokenManager readonly #getTokenPoolState = new GetTokenPoolState() readonly #initChainRemoteConfig = new InitChainRemoteConfig() readonly #removeFromAllowlist = new RemoveFromAllowlist() + readonly #setChainRateLimit = new SetChainRateLimit() + readonly #setRateLimitAdmin = new SetRateLimitAdmin() /** Creates a Solana CCT manager for an existing chain. */ constructor(chain: SolanaChain) { @@ -646,6 +658,136 @@ export class SolanaTokenManager extends TokenManager 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 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 diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index 9a36449f9..c1ad85146 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -6,3 +6,5 @@ export * from './edit-chain-remote-config.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' 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 000000000..7d322eebb --- /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 000000000..24ba14cc0 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/set-chain-rate-limit.ts @@ -0,0 +1,226 @@ +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 { + parsePublicKey, + resolvePoolProgram, + validateAuthorityMatchesWallet, + validateBigInt, +} from '../../validate.ts' + +const U64_MAX = 0xffff_ffff_ffff_ffffn + +/** + * 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 000000000..d62b296b1 --- /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 000000000..c9e7ecdf4 --- /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) + } +} From ddbe59c31a222e87d80379980734984ecd48006c Mon Sep 17 00:00:00 2001 From: Mervin Date: Wed, 12 Aug 2026 22:14:01 +0800 Subject: [PATCH 62/87] fix(cct-sdk): Refactor parse and validations for old solana ops (#350) * fix: refactor solana parse with validations * fix: remove TODO * fix: address comments --- ccip-sdk/src/cct/solana/operation.ts | 1 - .../operations/append-to-lookup-table.ts | 89 +++++++++++-------- .../operations/create-lookup-table.ts | 80 +++++++++-------- .../operations/register-admin.ts | 73 ++++++++------- .../operations/set-pool.ts | 54 ++++++----- .../operations/transfer-admin.ts | 68 +++++++------- .../operations/create-token-multisig.ts | 68 +++++++------- .../operations/deploy-token-pool.ts | 71 ++++++++------- .../token/operations/create-token-account.ts | 39 ++++---- .../solana/token/operations/deploy-token.ts | 27 +++--- 10 files changed, 313 insertions(+), 257 deletions(-) diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts index 03272cb18..2b1e23504 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -20,7 +20,6 @@ export type SolanaExecuteParams

= P & { computeUnits?: number } -// TODO: migrate remaining Solana operations to parse normalized params. /** * Solana CCT write base. Subclasses supply {@link parse} and {@link buildUnsigned}. * 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 index baea1e28f..9766a389c 100644 --- 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 @@ -1,9 +1,12 @@ -import { type TransactionInstruction, AddressLookupTableProgram, PublicKey } from '@solana/web3.js' +import { + type PublicKey, + type TransactionInstruction, + AddressLookupTableProgram, +} 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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' import { @@ -15,10 +18,9 @@ import { deriveCcipLookupTableAddresses } from '../../programs/alt.ts' import type { PoolProgramRef } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' import { + parsePublicKey, resolvePoolProgram, validateAuthorityMatchesWallet, - validateOptionalPublicKey, - validatePublicKey, } from '../../validate.ts' const MAX_ALT_ADDRESSES = 256 @@ -53,6 +55,15 @@ type AppendToLookupTableParams = { /** 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 @@ -65,17 +76,25 @@ export type ExecuteAppendToLookupTableResult = TransactionResult /** Builds and submits Solana ALT extend instructions for token pool setup. */ export class AppendToLookupTable extends SolanaOperation< AppendToLookupTableParams, - GenerateAppendToLookupTableResult + GenerateAppendToLookupTableResult, + ParsedAppendToLookupTableParams > { readonly name = 'appendToLookupTable' /** Parses all public keys before any RPC. */ protected override parse( params: GenerateAppendToLookupTableParams, - ): GenerateAppendToLookupTableParams { - validatePublicKey(this.name, 'lookupTableAddress', params.lookupTableAddress) - validatePublicKey(this.name, 'payer', params.payer) - validateOptionalPublicKey(this.name, 'authority', params.authority) + ): 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 @@ -87,11 +106,14 @@ export class AppendToLookupTable extends SolanaOperation< 'tokenAddress and exactly one of poolType or poolProgramAddress must be provided together', ) } - validateOptionalPublicKey(this.name, 'tokenAddress', params.tokenAddress) - if (hasPoolProgram) resolvePoolProgram(this.name, params) - for (const [i, address] of (params.additionalAddresses ?? []).entries()) { - validatePublicKey(this.name, `additionalAddresses[${i}]`, address) - } + 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) { @@ -101,19 +123,22 @@ export class AppendToLookupTable extends SolanaOperation< 'must provide tokenAddress/poolProgramAddress or additionalAddresses', ) } - return params + return { + payer, + authority, + lookupTableAddress, + additionalAddresses, + ...(tokenMint !== undefined && { tokenMint }), + ...(poolProgram !== undefined && { poolProgram }), + } } /** Builds unsigned ALT extend instructions. */ protected async buildUnsigned( chain: SolanaChain, - opts: GenerateAppendToLookupTableParams, + opts: ParsedAppendToLookupTableParams, ): Promise { - const payer = new PublicKey(opts.payer) - const authority = new PublicKey(opts.authority ?? opts.payer) - const lookupTableAddress = new PublicKey(opts.lookupTableAddress) - const poolProgram = - opts.tokenAddress !== undefined ? resolvePoolProgram(this.name, opts) : undefined + const { payer, authority, lookupTableAddress, poolProgram } = opts const lookupTable = await chain.connection.getAddressLookupTable(lookupTableAddress) if (!lookupTable.value) { @@ -132,10 +157,10 @@ export class AppendToLookupTable extends SolanaOperation< ) } - const addresses = [...(opts.additionalAddresses ?? []).map((a) => new PublicKey(a))] + const addresses = [...opts.additionalAddresses] - if (opts.tokenAddress !== undefined && poolProgram) { - const tokenMint = new PublicKey(opts.tokenAddress) + if (opts.tokenMint && poolProgram) { + const { tokenMint } = opts const ccipAddresses = await deriveCcipLookupTableAddresses(chain, { lookupTableAddress, tokenMint, @@ -192,24 +217,18 @@ export class AppendToLookupTable extends SolanaOperation< chain: SolanaChain, params: ExecuteAppendToLookupTableParams, ): Promise { - const { wallet, computeUnits, ...rest } = params - if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) - - const payer = wallet.publicKey.toBase58() - const generateParams: GenerateAppendToLookupTableParams = { ...rest, payer } - this.parse(generateParams) + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) - const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined - if (authority) { + if (params.authority !== undefined) { validateAuthorityMatchesWallet( this.name, - authority, + 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, generateParams) + 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.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/create-lookup-table.ts index 666996479..4f04a6842 100644 --- 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 @@ -1,9 +1,12 @@ -import { type TransactionInstruction, AddressLookupTableProgram, PublicKey } from '@solana/web3.js' +import { + type PublicKey, + type TransactionInstruction, + AddressLookupTableProgram, +} 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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' import { @@ -18,10 +21,9 @@ import { import type { PoolProgramRef } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' import { + parsePublicKey, resolvePoolProgram, validateAuthorityMatchesWallet, - validateOptionalPublicKey, - validatePublicKey, } from '../../validate.ts' const MAX_ALT_ADDRESSES = 256 @@ -49,6 +51,17 @@ type CreateLookupTableParams = /** 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 @@ -63,33 +76,38 @@ export type ExecuteCreateLookupTableResult = TransactionResult & { lookupTableAd /** Builds and submits Solana ALT create instructions, optionally with extend instructions. */ export class CreateLookupTable extends SolanaOperation< CreateLookupTableParams, - GenerateCreateLookupTableResult + GenerateCreateLookupTableResult, + ParsedCreateLookupTableParams > { readonly name = 'createLookupTable' /** Parses params before `buildUnsigned()` performs any RPC. */ - protected override parse( - params: GenerateCreateLookupTableParams, - ): GenerateCreateLookupTableParams { - validatePublicKey(this.name, 'payer', params.payer) - validateOptionalPublicKey(this.name, 'authority', params.authority) - if (params.mode === 'createEmpty') return params - - validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) - resolvePoolProgram(this.name, params) - for (const [i, address] of (params.additionalAddresses ?? []).entries()) { - validatePublicKey(this.name, `additionalAddresses[${i}]`, address) + 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), + ), } - return params } /** Builds unsigned ALT create instructions, optionally with extend instructions. */ protected async buildUnsigned( chain: SolanaChain, - opts: GenerateCreateLookupTableParams, + opts: ParsedCreateLookupTableParams, ): Promise { - const payer = new PublicKey(opts.payer) - const authority = new PublicKey(opts.authority ?? opts.payer) + const { payer, authority } = opts if (opts.mode === 'createEmpty') { const { instruction: createIx, lookupTableAddress } = buildCreateLookupTableInstruction({ @@ -108,10 +126,7 @@ export class CreateLookupTable extends SolanaOperation< } } - // Validate and parse the pool program before calling slot RPC below. - const poolProgram = resolvePoolProgram(this.name, opts) - const tokenMint = new PublicKey(opts.tokenAddress) - const additionalAddresses = (opts.additionalAddresses ?? []).map((a) => new PublicKey(a)) + const { poolProgram, tokenMint, additionalAddresses } = opts const { instruction: createIx, lookupTableAddress } = buildCreateLookupTableInstruction({ authority, @@ -162,25 +177,18 @@ export class CreateLookupTable extends SolanaOperation< chain: SolanaChain, params: ExecuteCreateLookupTableParams, ): Promise { - const { wallet, computeUnits, ...rest } = params - if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) - - const payer = wallet.publicKey.toBase58() - const generateParams: GenerateCreateLookupTableParams = { ...rest, payer } + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) - this.parse(generateParams) - - const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined - if (params.mode !== 'createEmpty' && authority) { + if (params.mode !== 'createEmpty' && params.authority !== undefined) { validateAuthorityMatchesWallet( this.name, - authority, + 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, generateParams) + 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/register-admin.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/register-admin.ts index e90063243..3df931738 100644 --- 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 @@ -1,10 +1,9 @@ import { unpackMint } from '@solana/spl-token' import { type TransactionInstruction, 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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' import { resolveTokenMint } from '../../../../solana/utils.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' @@ -19,11 +18,7 @@ import { deriveTokenAdminRegistryPda, } from '../../programs/router.ts' import { submit } from '../../submit.ts' -import { - validateAuthorityMatchesWallet, - validateOptionalPublicKey, - validatePublicKey, -} from '../../validate.ts' +import { parsePublicKey, validateAuthorityMatchesWallet } from '../../validate.ts' /** Authorization paths used to register a token in the TokenAdminRegistry. */ const REGISTER_ADMIN_METHODS = { @@ -57,6 +52,15 @@ type RegisterAdminParams = { /** 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 @@ -131,16 +135,15 @@ async function buildCcipAdminInstruction( } /** Registers a token through either its mint authority or the Router CCIP admin. */ -export class RegisterAdmin extends SolanaOperation { +export class RegisterAdmin extends SolanaOperation< + RegisterAdminParams, + UnsignedSolanaTx, + ParsedRegisterAdminParams +> { readonly name = 'registerAdmin' /** Parses all caller-supplied parameters before RPC. */ - protected override parse(params: GenerateRegisterAdminParams): GenerateRegisterAdminParams { - validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) - validatePublicKey(this.name, 'address', params.address) - validatePublicKey(this.name, 'payer', params.payer) - validateOptionalPublicKey(this.name, 'administrator', params.administrator) - validateOptionalPublicKey(this.name, 'authority', params.authority) + protected override parse(params: GenerateRegisterAdminParams): ParsedRegisterAdminParams { if ( params.registrationMethod !== undefined && !Object.values(REGISTER_ADMIN_METHODS).includes(params.registrationMethod) @@ -151,25 +154,33 @@ export class RegisterAdmin extends SolanaOperation { 'must be owner or ccip-admin', ) } - return params + 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: GenerateRegisterAdminParams, + opts: ParsedRegisterAdminParams, ): Promise { - const routerAddress = await chain.getTokenAdminRegistryFor(opts.address) - const router = new PublicKey(routerAddress) - const tokenMint = new PublicKey(opts.tokenAddress) - const payer = new PublicKey(opts.payer) - const authority = new PublicKey(opts.authority ?? opts.payer) + 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 !== undefined ? new PublicKey(opts.administrator) : mintAuthority - const method = opts.registrationMethod ?? REGISTER_ADMIN_METHODS.OWNER + const administrator = opts.administrator ?? mintAuthority const config = deriveRouterConfigPda(router) const tokenAdminRegistry = deriveTokenAdminRegistryPda(router, tokenMint) if (await chain.connection.getAccountInfo(tokenAdminRegistry)) { @@ -223,24 +234,18 @@ export class RegisterAdmin extends SolanaOperation { chain: SolanaChain, params: ExecuteRegisterAdminParams, ): Promise { - const { wallet, computeUnits, ...rest } = params - if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) - - const payer = wallet.publicKey.toBase58() - const generateParams: GenerateRegisterAdminParams = { ...rest, payer } - this.parse(generateParams) + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) - const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined - if (authority) { + if (params.authority !== undefined) { validateAuthorityMatchesWallet( this.name, - authority, + parsed.authority, wallet.publicKey, 'registerAdmin requires authority to be the executing wallet. Use generateUnsignedRegisterAdmin for externally signed transactions.', ) } - const tx = await this.buildUnsigned(chain, generateParams) + 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.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/set-pool.ts index e6d777d6f..bfcb5a0f5 100644 --- 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 @@ -16,11 +16,7 @@ import { deriveRouterConfigPda, deriveTokenAdminRegistryPda, } from '../../programs/router.ts' -import { - validateOptionalPublicKey, - validatePublicKey, - validateWritableIndexes, -} from '../../validate.ts' +import { parsePublicKey, validateWritableIndexes } from '../../validate.ts' /** Standard BurnMint/LockRelease pool ALT writable positions. */ export const DEFAULT_WRITABLE_INDEXES = [3, 4, 7] as const @@ -52,6 +48,15 @@ type SetPoolParams = { /** 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 @@ -62,39 +67,44 @@ export type ExecuteSetPoolParams = SolanaExecuteParams export type ExecuteSetPoolResult = TransactionResult /** Solana TokenAdminRegistry `setPool` operation. */ -export class SetPool extends SolanaOperation { +export class SetPool extends SolanaOperation { readonly name = 'setPool' /** Parses all public keys before any RPC. */ - protected override parse(params: GenerateSetPoolParams): GenerateSetPoolParams { - validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) - validatePublicKey(this.name, 'address', params.address) - validatePublicKey(this.name, 'poolLookupTableAddress', params.poolLookupTableAddress) - validatePublicKey(this.name, 'payer', params.payer) - validateOptionalPublicKey(this.name, 'authority', params.authority) + protected override parse(params: GenerateSetPoolParams): ParsedSetPoolParams { validateWritableIndexes(this.name, 'writableIndexes', params.writableIndexes) - return params + 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: GenerateSetPoolParams, + opts: ParsedSetPoolParams, ): Promise { - const routerAddress = await chain.getTokenAdminRegistryFor(opts.address) - const router = new PublicKey(routerAddress) - const tokenMint = new PublicKey(opts.tokenAddress) - const payer = new PublicKey(opts.payer) - const authority = new PublicKey(opts.authority ?? opts.payer) - const lookupTable = new PublicKey(opts.poolLookupTableAddress) + 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 writableIndexes = opts.writableIndexes ?? [...DEFAULT_WRITABLE_INDEXES] const instruction = await routerProgram.methods - .setPool(Buffer.from(writableIndexes)) + .setPool(Buffer.from(opts.writableIndexes)) .accounts({ config, tokenAdminRegistry, 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 index 2438c4b37..3b125e1ab 100644 --- 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 @@ -1,9 +1,8 @@ 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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' import { @@ -17,11 +16,7 @@ import { deriveTokenAdminRegistryPda, } from '../../programs/router.ts' import { submit } from '../../submit.ts' -import { - validateAuthorityMatchesWallet, - validateOptionalPublicKey, - validatePublicKey, -} from '../../validate.ts' +import { parsePublicKey, validateAuthorityMatchesWallet } from '../../validate.ts' /** Parameters shared by Solana TokenAdminRegistry `transferAdmin` generation and execution. */ type TransferAdminParams = { @@ -40,6 +35,14 @@ type TransferAdminParams = { /** 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 @@ -50,29 +53,35 @@ export type ExecuteTransferAdminParams = SolanaExecuteParams { +export class TransferAdmin extends SolanaOperation< + TransferAdminParams, + UnsignedSolanaTx, + ParsedTransferAdminParams +> { readonly name = 'transferAdmin' /** Parses all public keys before any RPC. */ - protected override parse(params: GenerateTransferAdminParams): GenerateTransferAdminParams { - validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) - validatePublicKey(this.name, 'address', params.address) - validatePublicKey(this.name, 'newAdmin', params.newAdmin) - validatePublicKey(this.name, 'payer', params.payer) - validateOptionalPublicKey(this.name, 'authority', params.authority) - return params + 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: GenerateTransferAdminParams, + opts: ParsedTransferAdminParams, ): Promise { - const tokenMint = new PublicKey(opts.tokenAddress) - const payer = new PublicKey(opts.payer) - const authority = new PublicKey(opts.authority ?? opts.payer) - const newAdmin = new PublicKey(opts.newAdmin) - const router = new PublicKey(await chain.getTokenAdminRegistryFor(opts.address)) + 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)) { @@ -107,28 +116,17 @@ export class TransferAdmin extends SolanaOperation { chain: SolanaChain, params: ExecuteTransferAdminParams, ): Promise { - const { wallet, computeUnits, ...rest } = params - if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) - - const payer = wallet.publicKey.toBase58() - const generateParams: GenerateTransferAdminParams = { ...rest, payer } - this.parse(generateParams) + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) if (params.authority !== undefined) { validateAuthorityMatchesWallet( this.name, - new PublicKey(params.authority), + 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, generateParams), - this.name, - computeUnits, - ) + 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.ts b/ccip-sdk/src/cct/solana/token-pool/operations/create-token-multisig.ts index 2a43a95c0..8eccfb7dd 100644 --- 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 @@ -2,10 +2,9 @@ import { MULTISIG_SIZE, createInitializeMultisigInstruction, unpackMint } from ' import { PublicKey, SystemProgram } from '@solana/web3.js' import { concat, hexlify, randomBytes, sha256, toUtf8Bytes } from 'ethers' -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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' import { resolveTokenMint } from '../../../../solana/utils.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' @@ -21,12 +20,11 @@ import { } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' import { + parsePublicKey, validateAuthorityMatchesWallet, validateInteger, validateNonEmptyString, validatePoolType, - validatePublicKey, - validatePublicKeys, } from '../../validate.ts' export const SOLANA_MULTISIG_MAX_SIGNERS = 11 @@ -52,6 +50,15 @@ type CreateTokenMultisigParams = { /** 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 } @@ -129,34 +136,41 @@ function getMintAuthority( */ export class CreateTokenMultisig extends SolanaOperation< CreateTokenMultisigParams, - GenerateCreateTokenMultisigResult + GenerateCreateTokenMultisigResult, + ParsedCreateTokenMultisigParams > { readonly name = 'createTokenMultisig' /** Parses public keys, threshold, and optional seed before mint/account RPCs. */ protected override parse( params: GenerateCreateTokenMultisigParams, - ): GenerateCreateTokenMultisigParams { - validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + ): ParsedCreateTokenMultisigParams { validatePoolType(this.name, 'poolType', params.poolType) - validatePublicKey(this.name, 'payer', params.payer) - if (params.additionalSigners !== undefined) { - validatePublicKeys(this.name, 'additionalSigners', params.additionalSigners) + 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 params + + 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: GenerateCreateTokenMultisigParams, + opts: ParsedCreateTokenMultisigParams, mintContext?: { account: MintAccount; authority: PublicKey }, ): Promise { - const payer = new PublicKey(opts.payer) - const tokenMint = new PublicKey(opts.tokenAddress) - const poolProgram = resolveTokenPoolProgram(opts.poolType) + const { payer, tokenMint, poolProgram } = opts const mintAccount = mintContext?.account ?? (await resolveTokenMint(chain.connection, tokenMint)) @@ -165,10 +179,9 @@ export class CreateTokenMultisig extends SolanaOperation< mintContext?.authority ?? getMintAuthority(this.name, tokenMint, mintAccount, tokenProgram) const poolSigner = deriveTokenPoolSignerPda(poolProgram, tokenMint) - const nonPoolSigners = dedupePublicKeys([ - authority, - ...(opts.additionalSigners ?? []).map((signer) => new PublicKey(signer)), - ]).filter((signer) => !signer.equals(poolSigner)) + 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) @@ -209,20 +222,11 @@ export class CreateTokenMultisig extends SolanaOperation< chain: SolanaChain, params: ExecuteCreateTokenMultisigParams, ): Promise { - const { wallet, computeUnits, ...rest } = params - if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) - - const generateParams: GenerateCreateTokenMultisigParams = { - ...rest, - payer: wallet.publicKey.toBase58(), - } - this.parse(generateParams) - - const tokenMint = new PublicKey(generateParams.tokenAddress) - const mintAccount = await resolveTokenMint(chain.connection, tokenMint) + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + const mintAccount = await resolveTokenMint(chain.connection, parsed.tokenMint) const tokenProgram = mintAccount.owner - const mintAuthority = getMintAuthority(this.name, tokenMint, mintAccount, tokenProgram) + const mintAuthority = getMintAuthority(this.name, parsed.tokenMint, mintAccount, tokenProgram) validateAuthorityMatchesWallet( this.name, mintAuthority, @@ -230,7 +234,7 @@ export class CreateTokenMultisig extends SolanaOperation< '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, generateParams, { + const tx = await this.buildUnsigned(chain, parsed, { account: mintAccount, authority: mintAuthority, }) 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 index 80ed2f036..13f7684d2 100644 --- 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 @@ -1,9 +1,9 @@ -import { PublicKey, SystemProgram } from '@solana/web3.js' +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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' import { type SolanaExecuteParams, @@ -20,13 +20,7 @@ import { resolveTokenPoolProgram, } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' -import { - validateAuthorityMatchesWallet, - validateOptionalPublicKey, - validatePoolType, - validatePublicKey, - validatePublicKeys, -} from '../../validate.ts' +import { parsePublicKey, validateAuthorityMatchesWallet, validatePoolType } from '../../validate.ts' /** * Parameters for initializing a Solana token pool, optionally with an allowlist. @@ -55,6 +49,14 @@ type DeployTokenPoolParams = { /** 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 @@ -73,29 +75,39 @@ export type ExecuteDeployTokenPoolResult = TransactionResult & { /** Initializes a Solana token pool, optionally configuring an allowlist. */ export class DeployTokenPool extends SolanaOperation< DeployTokenPoolParams, - GenerateDeployTokenPoolResult + GenerateDeployTokenPoolResult, + ParsedDeployTokenPoolParams > { readonly name = 'deployTokenPool' /** Parses all public keys before any RPC. */ - protected override parse(params: GenerateDeployTokenPoolParams): GenerateDeployTokenPoolParams { - validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) + protected override parse(params: GenerateDeployTokenPoolParams): ParsedDeployTokenPoolParams { validatePoolType(this.name, 'poolType', params.poolType) - validatePublicKey(this.name, 'payer', params.payer) - validateOptionalPublicKey(this.name, 'authority', params.authority) - if (params.allowlist !== undefined) validatePublicKeys(this.name, 'allowlist', params.allowlist) - return params + 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: GenerateDeployTokenPoolParams, + opts: ParsedDeployTokenPoolParams, ): Promise { - const tokenMint = new PublicKey(opts.tokenAddress) - const poolProgram = resolveTokenPoolProgram(opts.poolType) - const payer = new PublicKey(opts.payer) - const authority = new PublicKey(opts.authority ?? opts.payer) + const { tokenMint, poolProgram, payer, authority, allowlist } = opts const program = createTokenPoolProgram(chain, poolProgram, payer) const state = deriveTokenPoolConfigPda(poolProgram, tokenMint) const poolSigner = deriveTokenPoolSignerPda(poolProgram, tokenMint) @@ -115,7 +127,6 @@ export class DeployTokenPool extends SolanaOperation< .instruction(), ] - const allowlist = (opts.allowlist ?? []).map((a) => new PublicKey(a)) if (allowlist.length) { instructions.push( await program.methods @@ -147,24 +158,18 @@ export class DeployTokenPool extends SolanaOperation< chain: SolanaChain, params: ExecuteDeployTokenPoolParams, ): Promise { - const { wallet, computeUnits, ...rest } = params - if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) - - const payer = wallet.publicKey.toBase58() - const generateParams: GenerateDeployTokenPoolParams = { ...rest, payer } - this.parse(generateParams) + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) - const authority = params.authority !== undefined ? new PublicKey(params.authority) : undefined - if (authority) { + if (params.authority !== undefined) { validateAuthorityMatchesWallet( this.name, - authority, + 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, generateParams) + const tx = await this.buildUnsigned(chain, parsed) const hash = await submit(chain, wallet, tx, this.name, computeUnits) return { ...hash, 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 index 79755b6be..bb0941dc1 100644 --- a/ccip-sdk/src/cct/solana/token/operations/create-token-account.ts +++ b/ccip-sdk/src/cct/solana/token/operations/create-token-account.ts @@ -1,10 +1,9 @@ import { createAssociatedTokenAccountIdempotentInstruction } from '@solana/spl-token' -import { PublicKey } from '@solana/web3.js' +import type { 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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' import { resolveATA } from '../../../../solana/utils.ts' import type { TransactionResult } from '../../../operation.ts' import { @@ -13,7 +12,7 @@ import { SolanaOperation, } from '../../operation.ts' import { submit } from '../../submit.ts' -import { validatePublicKey } from '../../validate.ts' +import { parsePublicKey } from '../../validate.ts' /** Parameters for deriving and creating a Solana associated token account. */ type CreateTokenAccountParams = { @@ -26,6 +25,12 @@ type CreateTokenAccountParams = { /** 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 } @@ -38,28 +43,28 @@ export type ExecuteCreateTokenAccountResult = TransactionResult & { tokenAccount /** Creates an Associated Token Account for any wallet or PDA owner. */ export class CreateTokenAccount extends SolanaOperation< CreateTokenAccountParams, - GenerateCreateTokenAccountResult + GenerateCreateTokenAccountResult, + ParsedCreateTokenAccountParams > { readonly name = 'createTokenAccount' /** Parses create-token-account parameters. */ protected override parse( params: GenerateCreateTokenAccountParams, - ): GenerateCreateTokenAccountParams { - validatePublicKey(this.name, 'payer', params.payer) - validatePublicKey(this.name, 'tokenAddress', params.tokenAddress) - validatePublicKey(this.name, 'ownerAddress', params.ownerAddress) - return params + ): 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: GenerateCreateTokenAccountParams, + params: ParsedCreateTokenAccountParams, ): Promise { - const payer = new PublicKey(params.payer) - const mint = new PublicKey(params.tokenAddress) - const owner = new PublicKey(params.ownerAddress) + const { payer, tokenAddress: mint, ownerAddress: owner } = params const { ata: tokenAccount, tokenProgram } = await resolveATA(chain.connection, mint, owner) chain.logger.debug( @@ -87,11 +92,11 @@ export class CreateTokenAccount extends SolanaOperation< chain: SolanaChain, params: ExecuteCreateTokenAccountParams, ): Promise { - const { wallet, computeUnits, ...rest } = params - if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) - const tx = await this.generate(chain, { ...rest, payer: wallet.publicKey.toBase58() }) + 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.ts b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts index baf9fbf6e..64501626d 100644 --- a/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts @@ -9,10 +9,9 @@ import { } from '@solana/spl-token' import { type TransactionInstruction, 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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' import { @@ -156,6 +155,8 @@ type DeployTokenConfig = { seed: string } +type ParsedDeployTokenParams = GenerateDeployTokenParams & { config: DeployTokenConfig } + function resolveDeployTokenConfig(params: GenerateDeployTokenParams): DeployTokenConfig { const payer = new PublicKey(params.payer) return { @@ -293,23 +294,27 @@ function validateMetaplexParams(operation: string, params: GenerateDeployTokenPa } /** Creates a Solana SPL mint, optionally with Metaplex metadata and initial supply. */ -export class DeployToken extends SolanaOperation { +export class DeployToken extends SolanaOperation< + DeployTokenParams, + GenerateDeployTokenResult, + ParsedDeployTokenParams +> { readonly name = 'deployToken' /** Parses mint and metadata params before any RPC. */ - protected override parse(params: GenerateDeployTokenParams): GenerateDeployTokenParams { + protected override parse(params: GenerateDeployTokenParams): ParsedDeployTokenParams { validateBaseParams(this.name, params) validatePreMintParams(this.name, params) validateMetaplexParams(this.name, params) - return params + return { ...params, config: resolveDeployTokenConfig(params) } } /** Builds the unsigned Solana mint creation instruction set. */ protected async buildUnsigned( chain: SolanaChain, - params: GenerateDeployTokenParams, + params: ParsedDeployTokenParams, ): Promise { - const config = resolveDeployTokenConfig(params) + 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) @@ -351,11 +356,9 @@ export class DeployToken extends SolanaOperation { - const { wallet, computeUnits, ...rest } = params - if (!isWallet(wallet)) throw new CCIPWalletInvalidError(wallet) + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + const externalSigner = getExternalMintAuthoritySigner(parsed, parsed.payer) - const payer = wallet.publicKey.toBase58() - const externalSigner = getExternalMintAuthoritySigner(rest, payer) if (externalSigner) { throw new CCTParamsInvalidError( this.name, @@ -364,7 +367,7 @@ export class DeployToken extends SolanaOperation Date: Thu, 13 Aug 2026 13:42:19 +0800 Subject: [PATCH 63/87] feat(cct-sdk): Add apply chain updates op solana (#349) * feat: add apply chian updates op solana * fix: add explicitly types * fix: address comments * fix: update tsdoc * fix: address comments * feat(cct-sdk): Add append remote pool addresses op solana (#352) * feat: add append remote pool addresses op solana * fix: address comments --- ccip-sdk/src/cct/solana/index.test.ts | 4 + ccip-sdk/src/cct/solana/index.ts | 167 +++++++ .../append-remote-pool-addresses.test.ts | 172 +++++++ .../append-remote-pool-addresses.ts | 174 +++++++ .../operations/apply-chain-updates.test.ts | 424 ++++++++++++++++++ .../operations/apply-chain-updates.ts | 386 ++++++++++++++++ .../operations/delete-chain-remote-config.ts | 3 +- .../edit-chain-remote-config.test.ts | 1 + .../operations/edit-chain-remote-config.ts | 6 +- .../cct/solana/token-pool/operations/index.ts | 2 + .../operations/init-chain-remote-config.ts | 3 +- .../operations/set-chain-rate-limit.ts | 3 +- ccip-sdk/src/cct/solana/validate.test.ts | 10 + ccip-sdk/src/cct/solana/validate.ts | 18 + 14 files changed, 1364 insertions(+), 9 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/append-remote-pool-addresses.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/append-remote-pool-addresses.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/apply-chain-updates.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/apply-chain-updates.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index f35ff0996..af3d3fa53 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -46,6 +46,10 @@ describe('SolanaTokenManager (cct/solana)', () => { 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') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 1a661b240..340d50c0f 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -64,6 +64,10 @@ import { type BaseGetTokenPoolStateResult, type BurnMintPoolProgramRef, type CustomPoolProgramRef, + type ExecuteAppendRemotePoolAddressesParams, + type ExecuteAppendRemotePoolAddressesResult, + type ExecuteApplyChainUpdatesParams, + type ExecuteApplyChainUpdatesResult, type ExecuteConfigureAllowlistParams, type ExecuteConfigureAllowlistResult, type ExecuteCreateTokenMultisigParams, @@ -82,6 +86,10 @@ import { type ExecuteSetChainRateLimitResult, type ExecuteSetRateLimitAdminParams, type ExecuteSetRateLimitAdminResult, + type GenerateAppendRemotePoolAddressesParams, + type GenerateAppendRemotePoolAddressesResult, + type GenerateApplyChainUpdatesParams, + type GenerateApplyChainUpdatesResult, type GenerateConfigureAllowlistParams, type GenerateConfigureAllowlistResult, type GenerateCreateTokenMultisigParams, @@ -104,6 +112,8 @@ import { type GetTokenPoolStateResult, type LockReleaseGetTokenPoolStateResult, type LockReleasePoolProgramRef, + AppendRemotePoolAddresses, + ApplyChainUpdates, ConfigureAllowlist, CreateTokenMultisig, DeleteChainRemoteConfig, @@ -133,6 +143,8 @@ export class SolanaTokenManager extends TokenManager readonly #transferAdmin = new TransferAdmin() // Token pool operations + readonly #appendRemotePoolAddresses = new AppendRemotePoolAddresses() + readonly #applyChainUpdates = new ApplyChainUpdates() readonly #configureAllowlist = new ConfigureAllowlist() readonly #createTokenMultisig = new CreateTokenMultisig() readonly #deployTokenPool = new DeployTokenPool() @@ -519,6 +531,161 @@ export class SolanaTokenManager extends TokenManager 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 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 000000000..894b44e96 --- /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 000000000..455a1536d --- /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 000000000..2c27bb231 --- /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 000000000..12747c3d2 --- /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/delete-chain-remote-config.ts b/ccip-sdk/src/cct/solana/token-pool/operations/delete-chain-remote-config.ts index 40d1e528b..85fa816bb 100644 --- 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 @@ -18,14 +18,13 @@ import { } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' import { + U64_MAX, parsePublicKey, resolvePoolProgram, validateAuthorityMatchesWallet, validateBigInt, } from '../../validate.ts' -const U64_MAX = 0xffff_ffff_ffff_ffffn - type DeleteChainRemoteConfigParams = PoolProgramRef & { /** Token mint address managed by the local pool. */ tokenAddress: string 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 index 64abd31d6..7ce1ebdd6 100644 --- 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 @@ -139,6 +139,7 @@ describe('EditChainRemoteConfig (cct/solana)', () => { [{ remoteChainSelector: -1n }, 'remoteChainSelector'], [{ remoteChainSelector: 1n << 64n }, 'remoteChainSelector'], [{ remoteTokenAddress: '0x123' }, 'remoteTokenAddress'], + [{ remotePoolAddresses: [''] }, 'remotePoolAddresses[0]'], [{ remotePoolAddresses: ['0x123'] }, 'remotePoolAddresses[0]'], [{ remotePoolAddresses: '0x12' }, 'remotePoolAddresses'], [{ remoteTokenDecimals: 256 }, 'remoteTokenDecimals'], 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 index 4f5d24616..fc5f46f44 100644 --- 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 @@ -21,7 +21,9 @@ import { } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' import { + U64_MAX, parseHexBytes, + parseNonEmptyHexBytes, parsePublicKey, resolvePoolProgram, validateAuthorityMatchesWallet, @@ -29,8 +31,6 @@ import { validateInteger, } from '../../validate.ts' -const U64_MAX = 0xffff_ffff_ffff_ffffn - /** Parameters shared by Solana token pool remote-config editing generation and execution. */ type EditChainRemoteConfigParams = PoolProgramRef & { /** Token mint address managed by the local pool. */ @@ -105,7 +105,7 @@ export class EditChainRemoteConfig extends SolanaOperation< throw new CCTParamsInvalidError(this.name, 'remotePoolAddresses', 'must be an array') } const remotePoolAddresses = params.remotePoolAddresses.map((address, i) => - parseHexBytes(this.name, `remotePoolAddresses[${i}]`, address), + parseNonEmptyHexBytes(this.name, `remotePoolAddresses[${i}]`, address), ) const payer = parsePublicKey(this.name, 'payer', params.payer) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index c1ad85146..0b07e7216 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -1,3 +1,5 @@ +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' 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 index fc1658753..03df8f043 100644 --- 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 @@ -21,6 +21,7 @@ import { } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' import { + U64_MAX, parseHexBytes, parsePublicKey, resolvePoolProgram, @@ -29,8 +30,6 @@ import { validateInteger, } from '../../validate.ts' -const U64_MAX = 0xffff_ffff_ffff_ffffn - /** Parameters shared by Solana token pool remote-config initialization generation and execution. */ type InitChainRemoteConfigParams = PoolProgramRef & { /** Token mint address managed by the local pool. */ 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 index 24ba14cc0..b0d4e7529 100644 --- 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 @@ -19,14 +19,13 @@ import { } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' import { + U64_MAX, parsePublicKey, resolvePoolProgram, validateAuthorityMatchesWallet, validateBigInt, } from '../../validate.ts' -const U64_MAX = 0xffff_ffff_ffff_ffffn - /** * Configuration for one direction of a token pool rate limiter. * diff --git a/ccip-sdk/src/cct/solana/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts index 1578fa9f2..9cdb4c8ff 100644 --- a/ccip-sdk/src/cct/solana/validate.test.ts +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -5,6 +5,7 @@ import { PublicKey } from '@solana/web3.js' import { parseHexBytes, + parseNonEmptyHexBytes, parsePublicKey, resolvePoolProgram, validateBigInt, @@ -37,6 +38,15 @@ describe('Validate (cct/solana)', () => { 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())) }) diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index 87f3a916d..f51836d7b 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -12,6 +12,9 @@ import { 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. @@ -228,3 +231,18 @@ export function parseHexBytes( } 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 +} From f63286839d49df5bccd253680238558165cd3bfe Mon Sep 17 00:00:00 2001 From: Mervin Date: Fri, 14 Aug 2026 00:01:13 +0800 Subject: [PATCH 64/87] feat(cct-sdk): Add get token pool remote op solana (#354) * feat: add apply chian updates op solana * feat: add append remote pool addresses op solana * feat: add get token pool remote op solana * fix: add tsdoc * fix: address comments --- ccip-sdk/src/cct/solana/index.test.ts | 1 + ccip-sdk/src/cct/solana/index.ts | 30 +++++++ .../operations/get-token-pool-remotes.test.ts | 90 +++++++++++++++++++ .../operations/get-token-pool-remotes.ts | 57 ++++++++++++ .../cct/solana/token-pool/operations/index.ts | 1 + 5 files changed, 179 insertions(+) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-remotes.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/get-token-pool-remotes.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index af3d3fa53..47df08776 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -66,6 +66,7 @@ describe('SolanaTokenManager (cct/solana)', () => { 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') }) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 340d50c0f..169450847 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -108,6 +108,8 @@ import { type GenerateSetChainRateLimitResult, type GenerateSetRateLimitAdminParams, type GenerateSetRateLimitAdminResult, + type GetTokenPoolRemotesParams, + type GetTokenPoolRemotesResult, type GetTokenPoolStateParams, type GetTokenPoolStateResult, type LockReleaseGetTokenPoolStateResult, @@ -119,6 +121,7 @@ import { DeleteChainRemoteConfig, DeployTokenPool, EditChainRemoteConfig, + GetTokenPoolRemotes, GetTokenPoolState, InitChainRemoteConfig, RemoveFromAllowlist, @@ -150,6 +153,7 @@ export class SolanaTokenManager extends TokenManager 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() @@ -1398,6 +1402,32 @@ export class SolanaTokenManager extends TokenManager 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`). 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 000000000..83dabc7cf --- /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 000000000..4cef282f7 --- /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/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index 0b07e7216..be3e686e0 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -5,6 +5,7 @@ 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' From e1be15a07db94d559f26637005a0da8ef3b9857b Mon Sep 17 00:00:00 2001 From: Mervin Date: Fri, 21 Aug 2026 00:59:37 +0800 Subject: [PATCH 65/87] feat(cct-sdk): Add set pool evm tests (#356) feat: add set pool evm test --- .../operations/set-pool.test.ts | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 ccip-sdk/src/cct/evm/token-admin-registry/operations/set-pool.test.ts 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 000000000..3cf7eb9e5 --- /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, + ) + }) + }) +}) From 4fd94b101b3d6a609bbd87548240e5fc4deb6f31 Mon Sep 17 00:00:00 2001 From: Mervin Date: Fri, 21 Aug 2026 14:47:31 +0800 Subject: [PATCH 66/87] feat(cct-sdk): Add transfer pool ownership op solana (#358) * feat: add transfer pool ownership op solana * fix: address comments * feat(cct-sdk): Add accept pool ownership op solana (#359) feat: add accept pool ownership op solana --- ccip-sdk/src/cct/solana/index.test.ts | 4 + ccip-sdk/src/cct/solana/index.ts | 127 +++++++++++ .../operations/accept-ownership.test.ts | 203 ++++++++++++++++++ .../token-pool/operations/accept-ownership.ts | 125 +++++++++++ .../cct/solana/token-pool/operations/index.ts | 2 + .../operations/transfer-ownership.test.ts | 187 ++++++++++++++++ .../operations/transfer-ownership.ts | 136 ++++++++++++ 7 files changed, 784 insertions(+) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/accept-ownership.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/accept-ownership.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/transfer-ownership.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/transfer-ownership.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 47df08776..f4c454a69 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -62,6 +62,10 @@ describe('SolanaTokenManager (cct/solana)', () => { 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') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 169450847..b1c4aac67 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -64,6 +64,8 @@ import { type BaseGetTokenPoolStateResult, type BurnMintPoolProgramRef, type CustomPoolProgramRef, + type ExecuteAcceptOwnershipParams, + type ExecuteAcceptOwnershipResult, type ExecuteAppendRemotePoolAddressesParams, type ExecuteAppendRemotePoolAddressesResult, type ExecuteApplyChainUpdatesParams, @@ -86,6 +88,10 @@ import { type ExecuteSetChainRateLimitResult, type ExecuteSetRateLimitAdminParams, type ExecuteSetRateLimitAdminResult, + type ExecuteTransferOwnershipParams, + type ExecuteTransferOwnershipResult, + type GenerateAcceptOwnershipParams, + type GenerateAcceptOwnershipResult, type GenerateAppendRemotePoolAddressesParams, type GenerateAppendRemotePoolAddressesResult, type GenerateApplyChainUpdatesParams, @@ -108,12 +114,15 @@ import { 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, @@ -127,6 +136,7 @@ import { RemoveFromAllowlist, SetChainRateLimit, SetRateLimitAdmin, + TransferOwnership, } from './token-pool/operations/index.ts' /** CCT admin facade for Solana. */ @@ -146,6 +156,7 @@ export class SolanaTokenManager extends TokenManager readonly #transferAdmin = new TransferAdmin() // Token pool operations + readonly #acceptOwnership = new AcceptOwnership() readonly #appendRemotePoolAddresses = new AppendRemotePoolAddresses() readonly #applyChainUpdates = new ApplyChainUpdates() readonly #configureAllowlist = new ConfigureAllowlist() @@ -159,6 +170,7 @@ export class SolanaTokenManager extends TokenManager 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) { @@ -892,6 +904,121 @@ export class SolanaTokenManager extends TokenManager 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 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 000000000..f1c7b0a68 --- /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 000000000..f0fc111c5 --- /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/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index be3e686e0..c01db6416 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -1,3 +1,4 @@ +export * from './accept-ownership.ts' export * from './append-remote-pool-addresses.ts' export * from './apply-chain-updates.ts' export * from './configure-allowlist.ts' @@ -11,3 +12,4 @@ 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/transfer-ownership.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/transfer-ownership.test.ts new file mode 100644 index 000000000..612381bc3 --- /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 000000000..3273b78d4 --- /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) + } +} From ff3b2290071e10b42db6a705e41612a24bb34982 Mon Sep 17 00:00:00 2001 From: Mervin Date: Mon, 24 Aug 2026 19:12:21 +0800 Subject: [PATCH 67/87] feat(cct-sdk): Add transfer mint/freeze authority op solana (#361) * feat: add transfer pool ownership op solana * feat: add accept pool ownership op solana * feat: add transfer authority op solana * fix: update export barrel * fix: address comments * fix: revert register admin * feat(cct-sdk): Add mint tokens op solana (#365) * feat: add mint tokens op solana * fix: address comments --- ccip-sdk/src/cct/solana/index.test.ts | 13 +- ccip-sdk/src/cct/solana/index.ts | 155 +++++++++++ .../src/cct/solana/token/operations/index.ts | 9 + .../token/operations/mint-tokens.test.ts | 223 ++++++++++++++++ .../solana/token/operations/mint-tokens.ts | 159 ++++++++++++ .../operations/set-token-authority.test.ts | 242 ++++++++++++++++++ .../token/operations/set-token-authority.ts | 180 +++++++++++++ 7 files changed, 980 insertions(+), 1 deletion(-) create mode 100644 ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts create mode 100644 ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts create mode 100644 ccip-sdk/src/cct/solana/token/operations/set-token-authority.test.ts create mode 100644 ccip-sdk/src/cct/solana/token/operations/set-token-authority.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index f4c454a69..ddbb6de31 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -3,7 +3,7 @@ import { describe, it } from 'node:test' import { Connection } from '@solana/web3.js' -import { SolanaTokenManager } from './index.ts' +import { type TokenAuthorityType, SolanaTokenManager, TOKEN_AUTHORITY_TYPES } from './index.ts' import type { GetTokenPoolStateParams, GetTokenPoolStateResult, @@ -28,6 +28,10 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(typeof cct.deployToken, 'function') assert.equal(typeof cct.generateUnsignedCreateTokenAccount, 'function') assert.equal(typeof cct.createTokenAccount, 'function') + assert.equal(typeof cct.generateUnsignedMintTokens, 'function') + assert.equal(typeof cct.mintTokens, 'function') + assert.equal(typeof cct.generateUnsignedSetTokenAuthority, 'function') + assert.equal(typeof cct.setTokenAuthority, 'function') // Token admin registry operations assert.equal(typeof cct.generateUnsignedAcceptAdmin, 'function') @@ -74,6 +78,13 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(typeof cct.getTokenPoolState, 'function') }) + it('exports public token authority constants', () => { + const authorityType: TokenAuthorityType = TOKEN_AUTHORITY_TYPES.MINT + + assert.equal(authorityType, 'mint') + assert.equal(TOKEN_AUTHORITY_TYPES.FREEZE, 'freeze') + }) + it('creates from a connection provider', async (t) => { const chain = stubChain() const connection = new Connection('http://localhost:8899') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index b1c4aac67..33c4db74a 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -17,11 +17,21 @@ import { type ExecuteCreateTokenAccountResult, type ExecuteDeployTokenParams, type ExecuteDeployTokenResult, + type ExecuteMintTokensParams, + type ExecuteMintTokensResult, + type ExecuteSetTokenAuthorityParams, + type ExecuteSetTokenAuthorityResult, type GenerateCreateTokenAccountParams, type GenerateCreateTokenAccountResult, type GenerateDeployTokenParams, type GenerateDeployTokenResult, + type GenerateMintTokensParams, + type GenerateMintTokensResult, + type GenerateSetTokenAuthorityParams, + type GenerateSetTokenAuthorityResult, CreateTokenAccount, + MintTokens, + SetTokenAuthority, } from './token/operations/index.ts' import { type ExecuteAcceptAdminParams, @@ -144,6 +154,8 @@ export class SolanaTokenManager extends TokenManager readonly chain: SolanaChain // Token operations readonly #createTokenAccount = new CreateTokenAccount() + readonly #mintTokens = new MintTokens() + readonly #setTokenAuthority = new SetTokenAuthority() // Token admin registry operations readonly #acceptAdmin = new AcceptAdmin() @@ -309,6 +321,148 @@ export class SolanaTokenManager extends TokenManager return this.#createTokenAccount.execute(this.chain, opts) } + /** + * Builds unsigned instructions to mint SPL tokens to a recipient's existing associated token account. + * + * @remarks + * `amount` is in base units. The recipient ATA must already exist; use + * {@link generateUnsignedCreateTokenAccount} to create it. `authority` defaults to `payer`. For + * an SPL Token multisig authority, provide `multisigSigners` and collect member signatures + * externally. + * + * @throws {@link CCTParamsInvalidError} If an address, amount, or multisig signer 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 CCIPTokenAccountNotFoundError} If the recipient ATA is missing; create it first + * with {@link generateUnsignedCreateTokenAccount}. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedMintTokens({ + * payer: mintAuthority, + * tokenAddress: mint, + * recipient, + * amount: 1_000_000n, // One token for a mint with six decimals + * }) + * ``` + */ + generateUnsignedMintTokens(opts: GenerateMintTokensParams): Promise { + return this.#mintTokens.generate(this.chain, opts) + } + + /** + * Mints SPL tokens to a recipient's existing associated token account using the executing wallet. + * + * @remarks + * `amount` is in base units. The recipient ATA must already exist; use {@link createTokenAccount} + * to create it. SPL Token multisig authorities require `multisigSigners` and external member + * signatures; use {@link generateUnsignedMintTokens}. + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address, amount, or multisig signer is invalid, or + * `authority` does not match the executing wallet. + * @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 CCIPTokenAccountNotFoundError} If the recipient ATA is missing; create it first + * with {@link createTokenAccount}. + * @throws {@link CCTTxFailedError} If simulation or the SPL Token program rejects the transaction. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.mintTokens({ + * wallet, + * tokenAddress: mint, + * recipient, + * amount: 1_000_000n, // One token for a mint with six decimals + * }) + * ``` + */ + mintTokens(opts: ExecuteMintTokensParams): Promise { + return this.#mintTokens.execute(this.chain, opts) + } + + /** + * Builds unsigned instructions for an immediate SPL Token mint and/or freeze authority update. + * + * @see {@link setTokenAuthority} For wallet-based execution. + * + * @remarks + * ⚠️ **IRREVERSIBLE:** Setting `newAuthority` to null **permanently revokes** the selected authority + * roles for the SPL Token. Once revoked, the authority cannot be recovered or transferred. + * Example: revoked mint authority prevents anyone from minting tokens. Use with extreme caution. + * + * Once confirmed, the current authority loses the selected roles. Set `authorityTypes` to + * `['mint']`, `['freeze']`, or both. All selected roles must have the same current authority. The + * instructions are atomic: no role changes if any selected update fails. + * `authority` defaults to `payer`. For an SPL Token multisig authority, provide `multisigSigners` + * and collect member signatures externally. + * + * @throws {@link CCTParamsInvalidError} If an address or authority role selection 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.generateUnsignedSetTokenAuthority({ + * payer: currentAuthority, + * tokenAddress: mint, + * newAuthority, + * authorityTypes: ['mint'], + * }) + * ``` + * + * @example Permanently revoke mint authority + * ```ts + * const revokeUnsigned = await cct.generateUnsignedSetTokenAuthority({ + * payer: currentAuthority, + * tokenAddress: mint, + * newAuthority: null, // ⚠️ PERMANENT + * authorityTypes: ['mint'], + * }) + * ``` + */ + generateUnsignedSetTokenAuthority( + opts: GenerateSetTokenAuthorityParams, + ): Promise { + return this.#setTokenAuthority.generate(this.chain, opts) + } + + /** + * Immediately sets SPL Token mint and/or freeze authority using the executing wallet. + * + * @see {@link generateUnsignedSetTokenAuthority} For externally signed transactions. + * + * @remarks + * ⚠️ **IRREVERSIBLE:** Setting `newAuthority` to null **permanently revokes** the selected authority + * roles for the SPL Token. Once revoked, the authority cannot be recovered or transferred. + * Example: revoked mint authority prevents anyone from minting tokens. Use with extreme caution. + * + * Once confirmed, the current authority loses the selected roles. Set `authorityTypes` to + * `['mint']`, `['freeze']`, or both. All selected roles must have the same current authority. The + * transaction is atomic: no role changes if any selected update fails. + * SPL Token multisig authorities require `multisigSigners` and external member signatures; use + * {@link generateUnsignedSetTokenAuthority}. + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address or authority role selection is invalid, or + * `authority` does not match the executing wallet. + * @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 SPL Token program rejects the transaction. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.setTokenAuthority({ wallet, tokenAddress: mint, newAuthority, authorityTypes: ['mint'] }) + * ``` + */ + setTokenAuthority(opts: ExecuteSetTokenAuthorityParams): Promise { + return this.#setTokenAuthority.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. @@ -1671,6 +1825,7 @@ export { deriveTokenPoolSignerPda, resolveTokenPoolProgram, } from './programs/token-pool.ts' +export { TOKEN_AUTHORITY_TYPES } from './token/operations/set-token-authority.ts' export type { TransactionResult } from '../operation.ts' export type { SerializedSolanaTxEncoding } from './serialize.ts' export type * from './token/operations/index.ts' diff --git a/ccip-sdk/src/cct/solana/token/operations/index.ts b/ccip-sdk/src/cct/solana/token/operations/index.ts index e397c117a..bdd8185fb 100644 --- a/ccip-sdk/src/cct/solana/token/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token/operations/index.ts @@ -1,2 +1,11 @@ export * from './create-token-account.ts' export * from './deploy-token.ts' +export * from './mint-tokens.ts' +export { SetTokenAuthority } from './set-token-authority.ts' +export type { + ExecuteSetTokenAuthorityParams, + ExecuteSetTokenAuthorityResult, + GenerateSetTokenAuthorityParams, + GenerateSetTokenAuthorityResult, + TokenAuthorityType, +} from './set-token-authority.ts' diff --git a/ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts new file mode 100644 index 000000000..076fce004 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts @@ -0,0 +1,223 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, +} from '@solana/spl-token' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { + CCIPTokenAccountNotFoundError, + CCIPTokenMintInvalidError, + CCIPTokenMintNotFoundError, +} 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' +import { U64_MAX } from '../../validate.ts' + +const TOKEN = Keypair.generate().publicKey +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const RECIPIENT = Keypair.generate().publicKey +const MULTISIG = Keypair.generate().publicKey.toBase58() +const MULTISIG_SIGNER_1 = Keypair.generate().publicKey.toBase58() +const MULTISIG_SIGNER_2 = Keypair.generate().publicKey.toBase58() +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function chain(mintOwner: PublicKey | null = TOKEN_PROGRAM_ID): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async () => + mintOwner ? { owner: mintOwner, data: Buffer.alloc(165) } : null, + }, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(), + connection: { + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID, data: Buffer.alloc(165) }), + 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: Record = {}, mintOwner?: PublicKey | null) { + return SolanaTokenManager.fromChain(chain(mintOwner)).generateUnsignedMintTokens({ + payer: PAYER, + tokenAddress: TOKEN.toBase58(), + recipient: RECIPIENT.toBase58(), + amount: 1_000_000n, + authority: AUTHORITY, + ...opts, + }) +} + +describe('MintTokens (cct/solana)', () => { + describe('generate', () => { + it('mints to the recipient ATA using the detected token program', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const ata = getAssociatedTokenAddressSync(TOKEN, RECIPIENT, true, TOKEN_PROGRAM_ID) + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction.programId.toBase58(), TOKEN_PROGRAM_ID.toBase58()) + assert.equal(instruction.data[0], 7) // MintTo + assert.equal(instruction.keys[0]!.pubkey.toBase58(), TOKEN.toBase58()) + assert.equal(instruction.keys[1]!.pubkey.toBase58(), ata.toBase58()) + assert.equal(instruction.keys[2]!.pubkey.toBase58(), AUTHORITY) + assert.equal(instruction.data.readBigUInt64LE(1), 1_000_000n) + }) + + it('supports Token-2022 and SPL Token multisig authorities', async () => { + const unsigned = await generate( + { + authority: MULTISIG, + multisigSigners: [MULTISIG_SIGNER_1, MULTISIG_SIGNER_2], + }, + TOKEN_2022_PROGRAM_ID, + ) + + assert.equal(unsigned.instructions[0]!.programId.toBase58(), TOKEN_2022_PROGRAM_ID.toBase58()) + assert.deepEqual( + unsigned.instructions[0]!.keys.slice(2).map(({ pubkey, isSigner }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + })), + [ + { pubkey: MULTISIG, isSigner: false }, + { pubkey: MULTISIG_SIGNER_1, isSigner: true }, + { pubkey: MULTISIG_SIGNER_2, isSigner: true }, + ], + ) + }) + + it('rejects a missing recipient ATA before simulation', async () => { + const missingAtaChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async (address: PublicKey) => + address.equals(TOKEN) ? { owner: TOKEN_PROGRAM_ID } : null, + }, + } as unknown as SolanaChain + + await assert.rejects( + () => + SolanaTokenManager.fromChain(missingAtaChain).generateUnsignedMintTokens({ + payer: PAYER, + tokenAddress: TOKEN.toBase58(), + recipient: RECIPIENT.toBase58(), + amount: 1n, + }), + (error: unknown) => + error instanceof CCIPTokenAccountNotFoundError && + error.context.token === TOKEN.toBase58() && + error.context.holder === RECIPIENT.toBase58(), + ) + }) + + it('encodes the maximum u64 amount', async () => { + const unsigned = await generate({ amount: U64_MAX }) + assert.equal(unsigned.instructions[0]!.data.readBigUInt64LE(1), U64_MAX) + }) + + it('defaults authority to payer', async () => { + const unsigned = await generate({ authority: undefined }) + assert.equal(unsigned.instructions[0]!.keys[2]!.pubkey.toBase58(), PAYER) + }) + }) + + describe('validation', () => { + it('rejects invalid parameters', async () => { + for (const [opts, param] of [ + [{ tokenAddress: 'invalid' }, 'tokenAddress'], + [{ recipient: 'invalid' }, 'recipient'], + [{ authority: 'invalid' }, 'authority'], + [{ amount: 0n }, 'amount'], + [{ amount: 1 }, 'amount'], + [{ amount: U64_MAX + 1n }, 'amount'], + [{ multisigSigners: 'invalid' }, 'multisigSigners'], + [{ multisigSigners: ['invalid'] }, 'multisigSigners[0]'], + ] as const) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + + it('rejects missing and non-token mints', async () => { + await assert.rejects( + () => generate({}, null), + (err: unknown) => err instanceof CCIPTokenMintNotFoundError, + ) + await assert.rejects( + () => generate({}, Keypair.generate().publicKey), + (err: unknown) => err instanceof CCIPTokenMintInvalidError, + ) + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).mintTokens({ + tokenAddress: TOKEN.toBase58(), + recipient: RECIPIENT.toBase58(), + amount: 1n, + wallet: WALLET, + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('requires unsigned generation for SPL multisig authorities', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).mintTokens({ + tokenAddress: TOKEN.toBase58(), + recipient: RECIPIENT.toBase58(), + amount: 1n, + authority: MULTISIG, + multisigSigners: [MULTISIG_SIGNER_1], + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'multisigSigners', + ) + }) + + it('rejects a non-wallet authority for signed minting', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).mintTokens({ + tokenAddress: TOKEN.toBase58(), + recipient: RECIPIENT.toBase58(), + amount: 1n, + authority: AUTHORITY, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'mintTokens' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts new file mode 100644 index 000000000..d33be6e4b --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts @@ -0,0 +1,159 @@ +import { TokenAccountNotFoundError, createMintToInstruction, getAccount } from '@solana/spl-token' +import type { PublicKey, TransactionInstruction } from '@solana/web3.js' + +import { CCIPTokenAccountNotFoundError } from '../../../../errors/index.ts' +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 { 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 { + U64_MAX, + parsePublicKey, + validateAuthorityMatchesWallet, + validateBigInt, +} from '../../validate.ts' + +type MintTokensParams = { + /** SPL token mint address. */ + tokenAddress: string + /** + * Associated Token Account (ATA) address for the recipient on this token mint. + * ⚠️ ATA must already exist; use `createTokenAccount` if needed. + */ + recipient: string + /** + * Amount to mint in base units (not human-readable tokens). + * E.g., 1_000_000n with 6 decimals = 1 token. + * Maximum u64: 2^64 - 1. + */ + amount: bigint + /** Mint authority. Defaults to `payer` for single-signer transactions. */ + authority?: string + /** SPL Token multisig member addresses. Required when authority is an SPL Token multisig. */ + multisigSigners?: string[] +} + +type ParsedMintTokensParams = { + tokenAddress: PublicKey + recipient: PublicKey + amount: bigint + authority: PublicKey + multisigSigners: PublicKey[] +} + +/** Parameters for unsigned Solana SPL token minting. */ +export type GenerateMintTokensParams = SolanaGenerateParams + +/** Unsigned Solana SPL token minting result. */ +export type GenerateMintTokensResult = UnsignedSolanaTx + +/** Parameters for executing Solana SPL token minting. */ +export type ExecuteMintTokensParams = SolanaExecuteParams + +/** Result of executing Solana SPL token minting. */ +export type ExecuteMintTokensResult = TransactionResult + +/** Mints SPL tokens to a recipient's existing associated token account. */ +export class MintTokens extends SolanaOperation< + MintTokensParams, + UnsignedSolanaTx, + ParsedMintTokensParams +> { + readonly name = 'mintTokens' + + /** Parses public keys, amount, and optional SPL Token multisig signers. */ + protected override parse(params: GenerateMintTokensParams): ParsedMintTokensParams { + validateBigInt(this.name, 'amount', params.amount, 1n, U64_MAX) + if (params.multisigSigners !== undefined && !Array.isArray(params.multisigSigners)) { + throw new CCTParamsInvalidError(this.name, 'multisigSigners', 'must be an array') + } + + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + recipient: parsePublicKey(this.name, 'recipient', params.recipient), + amount: params.amount, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + multisigSigners: (params.multisigSigners ?? []).map((signer, i) => + parsePublicKey(this.name, `multisigSigners[${i}]`, signer), + ), + } + } + + /** Builds an SPL Token `MintTo` instruction for the recipient's associated token account. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedMintTokensParams, + ): Promise { + const { ata, tokenProgram } = await resolveATA( + chain.connection, + opts.tokenAddress, + opts.recipient, + ) + try { + await getAccount(chain.connection, ata, undefined, tokenProgram) + } catch (error) { + if (error instanceof TokenAccountNotFoundError) { + throw new CCIPTokenAccountNotFoundError( + opts.tokenAddress.toBase58(), + opts.recipient.toBase58(), + ) + } + throw error + } + + const instructions: TransactionInstruction[] = [ + createMintToInstruction( + opts.tokenAddress, + ata, + opts.authority, + opts.amount, + opts.multisigSigners, + tokenProgram, + ), + ] + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, recipient = ${opts.recipient.toBase58()}, amount = ${opts.amount}`, + ) + return { family: ChainFamily.Solana, instructions, mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the mint authority wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteMintTokensParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (parsed.multisigSigners.length > 0) { + throw new CCTParamsInvalidError( + this.name, + 'multisigSigners', + 'requires externally signed transactions; use generateUnsignedMintTokens', + ) + } + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'mintTokens requires authority to be the executing wallet. Use generateUnsignedMintTokens 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/set-token-authority.test.ts b/ccip-sdk/src/cct/solana/token/operations/set-token-authority.test.ts new file mode 100644 index 000000000..d61fae1b4 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/set-token-authority.test.ts @@ -0,0 +1,242 @@ +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 } from '@solana/web3.js' + +import { CCIPTokenMintInvalidError, CCIPTokenMintNotFoundError } 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 TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const NEW_AUTHORITY = Keypair.generate().publicKey.toBase58() +const MULTISIG = Keypair.generate().publicKey.toBase58() +const MULTISIG_SIGNER_1 = Keypair.generate().publicKey.toBase58() +const MULTISIG_SIGNER_2 = Keypair.generate().publicKey.toBase58() +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} + +function chain(mintOwner: PublicKey | null = TOKEN_PROGRAM_ID): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async () => (mintOwner ? { owner: mintOwner } : null), + }, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(), + connection: { + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }), + 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: Record = {}, mintOwner?: PublicKey | null) { + return SolanaTokenManager.fromChain(chain(mintOwner)).generateUnsignedSetTokenAuthority({ + tokenAddress: TOKEN, + payer: PAYER, + authority: AUTHORITY, + newAuthority: NEW_AUTHORITY, + authorityTypes: ['mint', 'freeze'], + ...opts, + }) +} + +describe('SetTokenAuthority (cct/solana)', () => { + describe('generate', () => { + it('builds selected mint and freeze authority updates', async () => { + const unsigned = await generate() + + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(unsigned.instructions.length, 2) + assert.deepEqual( + unsigned.instructions.map((instruction) => ({ + programId: instruction.programId.toBase58(), + authorityType: instruction.data[1], + mint: instruction.keys[0]!.pubkey.toBase58(), + authority: instruction.keys[1]!.pubkey.toBase58(), + newAuthority: instruction.data.subarray(3).toString('hex'), + })), + [ + { + programId: TOKEN_PROGRAM_ID.toBase58(), + authorityType: 0, // MintTokens + mint: TOKEN, + authority: AUTHORITY, + newAuthority: new PublicKey(NEW_AUTHORITY).toBuffer().toString('hex'), + }, + { + programId: TOKEN_PROGRAM_ID.toBase58(), + authorityType: 1, // FreezeAccount + mint: TOKEN, + authority: AUTHORITY, + newAuthority: new PublicKey(NEW_AUTHORITY).toBuffer().toString('hex'), + }, + ], + ) + }) + + it('builds only the selected authority update for Token-2022', async () => { + const unsigned = await generate({ authorityTypes: ['freeze'] }, TOKEN_2022_PROGRAM_ID) + + assert.equal(unsigned.instructions.length, 1) + assert.equal(unsigned.instructions[0]!.programId.toBase58(), TOKEN_2022_PROGRAM_ID.toBase58()) + assert.equal(unsigned.instructions[0]!.data[1], 1) // FreezeAccount + }) + + it('includes SPL multisig member signers', async () => { + const unsigned = await generate({ + authority: MULTISIG, + authorityTypes: ['mint'], + multisigSigners: [MULTISIG_SIGNER_1, MULTISIG_SIGNER_2], + }) + + assert.deepEqual( + unsigned.instructions[0]!.keys.map(({ pubkey, isSigner, isWritable }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + isWritable, + })), + [ + { pubkey: TOKEN, isSigner: false, isWritable: true }, + { pubkey: MULTISIG, isSigner: false, isWritable: false }, + { pubkey: MULTISIG_SIGNER_1, isSigner: true, isWritable: false }, + { pubkey: MULTISIG_SIGNER_2, isSigner: true, isWritable: false }, + ], + ) + }) + + it('builds authority revocation with a null new authority', async () => { + const unsigned = await generate({ authorityTypes: ['mint'], newAuthority: null }) + const [instruction] = unsigned.instructions + + assert.ok(instruction) + assert.equal(instruction.data[0], 6) // SetAuthority + assert.equal(instruction.data[1], 0) // MintTokens + assert.equal(instruction.data[2], 0) // COption::None + assert.equal(instruction.data.length, 3) + }) + + it('defaults authority to payer', async () => { + const unsigned = await generate({ authority: undefined }) + + assert.equal(unsigned.instructions[0]!.keys[1]!.pubkey.toBase58(), PAYER) + }) + }) + + describe('validation', () => { + it('rejects invalid public keys', async () => { + for (const param of ['tokenAddress', 'newAuthority', 'authority']) { + await assert.rejects( + () => generate({ [param]: 'invalid' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + + it('rejects invalid multisig signers', async () => { + for (const [multisigSigners, param] of [ + ['invalid', 'multisigSigners'], + [['invalid'], 'multisigSigners[0]'], + ]) { + await assert.rejects( + () => generate({ multisigSigners }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + + it('reports invalid authority role selections', async () => { + const cases: [unknown, string][] = [ + [undefined, 'must be an array'], + [[], 'must not be empty'], + [['mint', 'mint'], 'must not contain duplicates'], + [['close'], 'must contain only mint and/or freeze'], + ] + for (const [authorityTypes, message] of cases) { + await assert.rejects( + () => generate({ authorityTypes }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'authorityTypes' && + err.message.includes(message), + ) + } + }) + + it('rejects missing and non-token mints', async () => { + await assert.rejects( + () => generate({}, null), + (err: unknown) => err instanceof CCIPTokenMintNotFoundError, + ) + await assert.rejects( + () => generate({}, Keypair.generate().publicKey), + (err: unknown) => err instanceof CCIPTokenMintInvalidError, + ) + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).setTokenAuthority({ + tokenAddress: TOKEN, + newAuthority: NEW_AUTHORITY, + authorityTypes: ['mint'], + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('requires unsigned generation for SPL multisig authorities', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).setTokenAuthority({ + tokenAddress: TOKEN, + newAuthority: NEW_AUTHORITY, + authority: MULTISIG, + authorityTypes: ['mint'], + multisigSigners: [MULTISIG_SIGNER_1], + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'multisigSigners', + ) + }) + + it('rejects a non-wallet authority for signed updates', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).setTokenAuthority({ + tokenAddress: TOKEN, + newAuthority: NEW_AUTHORITY, + authority: AUTHORITY, + authorityTypes: ['mint'], + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setTokenAuthority' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/set-token-authority.ts b/ccip-sdk/src/cct/solana/token/operations/set-token-authority.ts new file mode 100644 index 000000000..e7e1e6ee0 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/set-token-authority.ts @@ -0,0 +1,180 @@ +import { AuthorityType, createSetAuthorityInstruction } from '@solana/spl-token' +import type { PublicKey, TransactionInstruction } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import type { UnsignedSolanaTx } from '../../../../solana/types.ts' +import { resolveTokenProgram } 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 { submit } from '../../submit.ts' +import { parsePublicKey, validateAuthorityMatchesWallet } from '../../validate.ts' + +/** SPL Token authority roles that can be set. */ +export const TOKEN_AUTHORITY_TYPES = { + MINT: 'mint', + FREEZE: 'freeze', +} as const + +/** SPL Token authority role that can be set. */ +export type TokenAuthorityType = (typeof TOKEN_AUTHORITY_TYPES)[keyof typeof TOKEN_AUTHORITY_TYPES] + +type SetTokenAuthorityParams = { + /** SPL token mint address. */ + tokenAddress: string + /** Address to receive the selected authority roles, or **null to permanently revoke** them. ⚠️ Revocation is irreversible. */ + newAuthority: string | null + /** Current authority. Defaults to `payer` for single-signer transactions. */ + authority?: string + /** SPL Token multisig member addresses. Required when authority is an SPL Token multisig. */ + multisigSigners?: string[] + /** + * Authority roles to set. Specify `['mint']`, `['freeze']`, or `['mint', 'freeze']`. + * The same new authority or revocation applies to every selected role. To set roles to different + * authorities, make separate calls. + */ + authorityTypes: TokenAuthorityType[] +} + +type ParsedSetTokenAuthorityParams = { + tokenAddress: PublicKey + newAuthority: PublicKey | null + authority: PublicKey + multisigSigners: PublicKey[] + authorityTypes: TokenAuthorityType[] +} + +/** Parameters for unsigned Solana SPL Token authority update. */ +export type GenerateSetTokenAuthorityParams = SolanaGenerateParams + +/** Unsigned Solana SPL Token authority update result. */ +export type GenerateSetTokenAuthorityResult = UnsignedSolanaTx + +/** Parameters for executing Solana SPL Token authority update. */ +export type ExecuteSetTokenAuthorityParams = SolanaExecuteParams + +/** Result of executing Solana SPL Token authority update. */ +export type ExecuteSetTokenAuthorityResult = TransactionResult + +const SPL_AUTHORITY_TYPES: Record = { + mint: AuthorityType.MintTokens, + freeze: AuthorityType.FreezeAccount, +} + +/** + * Immediately sets mint authority, freeze authority, or both for an SPL Token mint; there is no + * propose-and-accept step. + * + * @remarks + * ⚠️ **IRREVERSIBLE:** Setting `newAuthority` to null permanently revokes the selected roles. + * Once revoked, a revoked mint or freeze authority **cannot be recovered, transferred, or restored**. + * + * Once confirmed, the current authority loses the selected roles. All selected roles must have the + * same current authority. Supply `multisigSigners` when that authority is an SPL Token multisig. + * **Atomic:** All selected roles update in one transaction. If any selected update fails, none are + * committed. + */ +export class SetTokenAuthority extends SolanaOperation< + SetTokenAuthorityParams, + UnsignedSolanaTx, + ParsedSetTokenAuthorityParams +> { + readonly name = 'setTokenAuthority' + + /** Parses public keys and validates the selected authority roles. */ + protected override parse(params: GenerateSetTokenAuthorityParams): ParsedSetTokenAuthorityParams { + const authorityTypes = params.authorityTypes + if (!Array.isArray(authorityTypes)) { + throw new CCTParamsInvalidError(this.name, 'authorityTypes', 'must be an array') + } + if (authorityTypes.length === 0) { + throw new CCTParamsInvalidError(this.name, 'authorityTypes', 'must not be empty') + } + if (new Set(authorityTypes).size !== authorityTypes.length) { + throw new CCTParamsInvalidError(this.name, 'authorityTypes', 'must not contain duplicates') + } + if (authorityTypes.some((type) => !Object.values(TOKEN_AUTHORITY_TYPES).includes(type))) { + throw new CCTParamsInvalidError( + this.name, + 'authorityTypes', + 'must contain only mint and/or freeze', + ) + } + + if (params.multisigSigners !== undefined && !Array.isArray(params.multisigSigners)) { + throw new CCTParamsInvalidError(this.name, 'multisigSigners', 'must be an array') + } + + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + newAuthority: + params.newAuthority === null + ? null + : parsePublicKey(this.name, 'newAuthority', params.newAuthority), + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + multisigSigners: (params.multisigSigners ?? []).map((signer, i) => + parsePublicKey(this.name, `multisigSigners[${i}]`, signer), + ), + authorityTypes, + } + } + + /** Builds one SPL Token `SetAuthority` instruction for each selected authority role. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedSetTokenAuthorityParams, + ): Promise { + const tokenProgram = await resolveTokenProgram(chain.connection, opts.tokenAddress) + const instructions: TransactionInstruction[] = opts.authorityTypes.map((authorityType) => + createSetAuthorityInstruction( + opts.tokenAddress, + opts.authority, + SPL_AUTHORITY_TYPES[authorityType], + opts.newAuthority, + opts.multisigSigners, + tokenProgram, + ), + ) + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, authorityTypes = ${opts.authorityTypes.join(',')}, newAuthority = ${opts.newAuthority?.toBase58() ?? 'revoked'}`, + ) + return { family: ChainFamily.Solana, instructions, mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the current authority wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteSetTokenAuthorityParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (parsed.multisigSigners.length > 0) { + throw new CCTParamsInvalidError( + this.name, + 'multisigSigners', + 'requires externally signed transactions; use generateUnsignedSetTokenAuthority', + ) + } + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'setTokenAuthority requires authority to be the executing wallet. Use generateUnsignedSetTokenAuthority for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} From 7d4e9db9d61985f9b260797e85bec56bb66dae18 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:10:53 -0300 Subject: [PATCH 68/87] feat(cct-sdk): Add get token pool remotes evm query (#379) --- ccip-sdk/src/cct/evm/index.ts | 38 ++++++ .../operations/get-token-pool-remotes.test.ts | 121 ++++++++++++++++++ .../operations/get-token-pool-remotes.ts | 63 +++++++++ ccip-sdk/src/cct/evm/validate.ts | 22 ++++ 4 files changed, 244 insertions(+) create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-remotes.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-remotes.ts diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index b0d41bc40..be74f3d8a 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -48,6 +48,11 @@ import { type DeployTokenPoolParams, DeployTokenPool, } from './token-pool/operations/deploy-token-pool.ts' +import { + type GetTokenPoolRemotesParams, + type GetTokenPoolRemotesResult, + GetTokenPoolRemotes, +} from './token-pool/operations/get-token-pool-remotes.ts' import { type GetTokenPoolStateParams, type GetTokenPoolStateResult, @@ -76,6 +81,7 @@ export class EVMTokenManager extends TokenManager { readonly #deployTokenPool = new DeployTokenPool() readonly #transferOwnership = new TransferOwnership() readonly #getTokenPoolState = new GetTokenPoolState() + readonly #getTokenPoolRemotes = new GetTokenPoolRemotes() // Lockbox operations readonly #deployLockbox = new DeployLockbox() @@ -592,6 +598,32 @@ export class EVMTokenManager extends TokenManager { getTokenPoolState(opts: GetTokenPoolStateParams): Promise { return this.#getTokenPoolState.query(this.chain, opts) } + + /** + * Reads a pool's remote-lane configuration, v1.5.0 through v2.0.0: for each configured remote + * chain, the `remoteToken`, the `remotePools` authorized to mint/release against it, and the + * inbound/outbound rate-limiter buckets. Keyed by remote network name. + * @remarks Omit `remoteChainSelector` to scan every lane the pool reports through + * `getSupportedChains()`; pass one to read a single lane. `inboundRateLimiterState` / + * `outboundRateLimiterState` are `null` when that direction is unlimited, and their amounts are + * in the *local* token's smallest unit; v2.0.0 pools add `fast*RateLimiterState` for + * Faster-Than-Finality and safe-finality (FCR) transfers. + * @throws {@link CCTParamsInvalidError} if `poolAddress` is not a valid address, or + * `remoteChainSelector` is given and is not a `uint64` + * @throws {@link CCIPTokenPoolChainConfigNotFoundError} if a lane read has no remote token + * configured — including a `remoteChainSelector` the pool knows nothing about + * @example + * ```typescript + * const remotes = await cct.getTokenPoolRemotes({ poolAddress: '0xPool...' }) + * for (const [network, lane] of Object.entries(remotes)) { + * // inboundRateLimiterState is null when inbound transfers are unlimited + * console.log(network, lane.remoteToken, lane.inboundRateLimiterState?.capacity) + * } + * ``` + */ + getTokenPoolRemotes(opts: GetTokenPoolRemotesParams): Promise { + return this.#getTokenPoolRemotes.query(this.chain, opts) + } } export * from '../errors.ts' @@ -623,6 +655,12 @@ export type { LockReleaseTokenPoolStateV2_0_0, TokenPoolStateV2_0_0, } from './token-pool/operations/get-token-pool-state.ts' +export type { + GetTokenPoolRemotesParams, + GetTokenPoolRemotesResult, +} from './token-pool/operations/get-token-pool-remotes.ts' +/** The lane types `GetTokenPoolRemotesResult` is keyed over; shared with `Chain.getTokenPoolRemotes`. */ +export type { RateLimiterState, TokenPoolRemote } from '../../chain.ts' export type { DeployLockboxParams } from './lockbox/operations/deploy-lockbox.ts' export type { AuthorizeLockboxCallersParams } from './lockbox/operations/authorize-callers.ts' export type { diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-remotes.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-remotes.test.ts new file mode 100644 index 000000000..27f1ac5f9 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-remotes.test.ts @@ -0,0 +1,121 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { GetTokenPoolRemotes } from './get-token-pool-remotes.ts' +import type { TokenPoolRemote } from '../../../../chain.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' + +const POOL = '0x' + '11'.repeat(20) +const SELECTOR = 5009297550715157269n + +const REMOTES: Record = { + 'ethereum-mainnet': { + remoteToken: '0x' + '22'.repeat(20), + remotePools: ['0x' + '33'.repeat(20)], + inboundRateLimiterState: { tokens: 25n, capacity: 50n, rate: 5n }, + outboundRateLimiterState: null, + }, +} + +/** Records every `getTokenPoolRemotes` call so tests can assert forwarding and RPC-freeness. */ +function stubChain(calls: Array<[string, bigint | undefined]> = []): EVMChain { + return { + getTokenPoolRemotes: (tokenPool: string, remoteChainSelector?: bigint) => { + calls.push([tokenPool, remoteChainSelector]) + return Promise.resolve(REMOTES) + }, + } as unknown as EVMChain +} + +describe('GetTokenPoolRemotes (cct/evm)', () => { + describe('query', () => { + it('forwards poolAddress and the selector to chain.getTokenPoolRemotes', async () => { + const calls: Array<[string, bigint | undefined]> = [] + const result = await new GetTokenPoolRemotes().query(stubChain(calls), { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + }) + + assert.equal(result, REMOTES) + assert.deepEqual(calls, [[POOL, SELECTOR]]) + }) + + it('omits the selector to scan every configured lane', async () => { + const calls: Array<[string, bigint | undefined]> = [] + const result = await new GetTokenPoolRemotes().query(stubChain(calls), { poolAddress: POOL }) + + assert.equal(result, REMOTES) + assert.deepEqual(calls, [[POOL, undefined]], 'no selector is forwarded as undefined') + }) + + it('passes the pool address through unnormalised, leaving resolution to the chain reader', async () => { + // the op is a thin delegate: it must not checksum, resolve, or otherwise rewrite the address + const calls: Array<[string, bigint | undefined]> = [] + const lowercase = POOL.toLowerCase() + await new GetTokenPoolRemotes().query(stubChain(calls), { poolAddress: lowercase }) + assert.equal(calls[0]![0], lowercase) + }) + + it('accepts the uint64 selector bounds', async () => { + const calls: Array<[string, bigint | undefined]> = [] + const chain = stubChain(calls) + for (const selector of [0n, 2n ** 64n - 1n]) { + await new GetTokenPoolRemotes().query(chain, { + poolAddress: POOL, + remoteChainSelector: selector, + }) + } + assert.deepEqual( + calls.map(([, selector]) => selector), + [0n, 2n ** 64n - 1n], + ) + }) + }) + + describe('validation', () => { + it('rejects an invalid poolAddress before any RPC', async () => { + let called = false + const chain = { + getTokenPoolRemotes: () => { + called = true + return Promise.resolve(REMOTES) + }, + } as unknown as EVMChain + + await assert.rejects( + () => new GetTokenPoolRemotes().query(chain, { poolAddress: 'not-an-address' }), + (error: unknown) => + error instanceof CCTParamsInvalidError && + error.context.operation === 'getTokenPoolRemotes' && + error.context.param === 'poolAddress', + ) + assert.equal(called, false, 'validation fails before the chain read') + }) + + for (const [label, remoteChainSelector] of [ + ['negative', -1n], + ['above uint64 max', 2n ** 64n], + ['a number, not a bigint', 1 as never], + ] as const) { + it(`rejects a remoteChainSelector that is ${label}, before any RPC`, async () => { + let called = false + const chain = { + getTokenPoolRemotes: () => { + called = true + return Promise.resolve(REMOTES) + }, + } as unknown as EVMChain + + await assert.rejects( + () => new GetTokenPoolRemotes().query(chain, { poolAddress: POOL, remoteChainSelector }), + (error: unknown) => + error instanceof CCTParamsInvalidError && + error.context.operation === 'getTokenPoolRemotes' && + error.context.param === 'remoteChainSelector', + ) + assert.equal(called, false, 'validation fails before the chain read') + }) + } + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-remotes.ts b/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-remotes.ts new file mode 100644 index 000000000..381c1cc55 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/get-token-pool-remotes.ts @@ -0,0 +1,63 @@ +/** + * getTokenPoolRemotes — reads a token pool's per-lane remote configuration + * Delegates to {@link EVMChain.getTokenPoolRemotes}. + * + * @packageDocumentation + */ + +import type { TokenPoolRemote } from '../../../../chain.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { EVMQuery } from '../../query.ts' +import { validateAddress, validateUint64 } from '../../validate.ts' + +/** Parameters for {@link GetTokenPoolRemotes}. */ +export type GetTokenPoolRemotesParams = { + /** + * Token pool contract address to read. + * @remarks Spelled `poolAddress` for consistency with every other CCT pool op, even though + * {@link EVMChain.getTokenPoolRemotes} names the same argument `tokenPool`. + */ + poolAddress: string + /** + * CCIP selector of a single remote chain to read (`uint64`). Omit to scan every lane the pool + * reports through `getSupportedChains()`. + */ + remoteChainSelector?: bigint +} + +/** Result of {@link GetTokenPoolRemotes}: remote-lane configurations keyed by network name. */ +export type GetTokenPoolRemotesResult = Record + +/** + * Reads all, or one selected, remote-chain configurations of an EVM token pool. + * + * @remarks Delegates decoding wholesale to {@link EVMChain.getTokenPoolRemotes}; this class only + * validates params (no RPC) and forwards them. + */ +export class GetTokenPoolRemotes extends EVMQuery< + GetTokenPoolRemotesParams, + GetTokenPoolRemotesResult +> { + readonly name = 'getTokenPoolRemotes' + + /** + * Validates the pool address and, when given, the remote-chain selector; nothing to convert for + * {@link read}. + * @throws {@link CCTParamsInvalidError} if `poolAddress` is not a valid address, or + * `remoteChainSelector` is given and is not a `uint64` + */ + protected prepare(params: GetTokenPoolRemotesParams): GetTokenPoolRemotesParams { + validateAddress(this.name, 'poolAddress', params.poolAddress) + if (params.remoteChainSelector !== undefined) + validateUint64(this.name, 'remoteChainSelector', params.remoteChainSelector) + return params + } + + /** Delegates remote-lane decoding to the shared chain reader, which owns the version branches. */ + protected read( + chain: EVMChain, + { poolAddress, remoteChainSelector }: GetTokenPoolRemotesParams, + ): Promise { + return chain.getTokenPoolRemotes(poolAddress, remoteChainSelector) + } +} diff --git a/ccip-sdk/src/cct/evm/validate.ts b/ccip-sdk/src/cct/evm/validate.ts index 5740169a4..096e72027 100644 --- a/ccip-sdk/src/cct/evm/validate.ts +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -71,6 +71,28 @@ export function validateUint8(operation: string, param: string, value: unknown): ) } +const UINT64_MAX = BigInt(2) ** BigInt(64) - 1n + +/** + * Asserts `value` is a `bigint` in `[0, 2^64 − 1]` (a Solidity `uint64`), narrowing it to + * `bigint` for callers. + * @remarks The width of a CCIP chain selector, so this is the check every `remoteChainSelector` + * param goes through. + * @throws {@link CCTParamsInvalidError} if `value` is not such a bigint + */ +export function validateUint64( + operation: string, + param: string, + value: unknown, +): asserts value is bigint { + if (typeof value === 'bigint' && value >= 0n && value <= UINT64_MAX) return + throw new CCTParamsInvalidError( + operation, + param, + `must be a bigint in [0, 2^64 − 1], got ${String(value)}`, + ) +} + /** Largest value representable by a Solidity `uint256`. */ const UINT256_MAX = BigInt(2) ** BigInt(256) - 1n From f9b7bdf0587ca7a7a257b498149432ae76e265f7 Mon Sep 17 00:00:00 2001 From: Mervin Date: Thu, 27 Aug 2026 10:51:59 +0800 Subject: [PATCH 69/87] fix(cct-sdk): Export Solana CCT configuration constants (#371) * fix: refactor public exports * fix: address comments --- ccip-sdk/src/cct/solana/index.test.ts | 15 +++++++++++++-- ccip-sdk/src/cct/solana/index.ts | 3 ++- .../solana/token-admin-registry/constants.ts | 11 +++++++++++ .../operations/register-admin.ts | 18 ++++++------------ .../operations/set-pool.test.ts | 8 +++++++- .../operations/set-pool.ts | 8 +++----- ccip-sdk/src/cct/solana/token/constants.ts | 5 +++++ .../src/cct/solana/token/operations/index.ts | 9 +-------- .../token/operations/set-token-authority.ts | 7 +------ 9 files changed, 49 insertions(+), 35 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/constants.ts create mode 100644 ccip-sdk/src/cct/solana/token/constants.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index ddbb6de31..4b3aa6202 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -3,7 +3,14 @@ import { describe, it } from 'node:test' import { Connection } from '@solana/web3.js' -import { type TokenAuthorityType, SolanaTokenManager, TOKEN_AUTHORITY_TYPES } from './index.ts' +import { + type RegisterAdminMethod, + type TokenAuthorityType, + DEFAULT_WRITABLE_INDEXES, + REGISTRATION_METHODS, + SolanaTokenManager, + TOKEN_AUTHORITY_TYPES, +} from './index.ts' import type { GetTokenPoolStateParams, GetTokenPoolStateResult, @@ -78,11 +85,15 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(typeof cct.getTokenPoolState, 'function') }) - it('exports public token authority constants', () => { + it('exports public CCT constants', () => { const authorityType: TokenAuthorityType = TOKEN_AUTHORITY_TYPES.MINT + const method: RegisterAdminMethod = REGISTRATION_METHODS.OWNER assert.equal(authorityType, 'mint') assert.equal(TOKEN_AUTHORITY_TYPES.FREEZE, 'freeze') + assert.equal(method, 'owner') + assert.equal(REGISTRATION_METHODS.CCIP_ADMIN, 'ccip-admin') + assert.deepEqual(DEFAULT_WRITABLE_INDEXES, [3, 4, 7]) }) it('creates from a connection provider', async (t) => { diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 33c4db74a..dda7ba2cf 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -1825,7 +1825,8 @@ export { deriveTokenPoolSignerPda, resolveTokenPoolProgram, } from './programs/token-pool.ts' -export { TOKEN_AUTHORITY_TYPES } from './token/operations/set-token-authority.ts' +export { TOKEN_AUTHORITY_TYPES } from './token/constants.ts' +export { DEFAULT_WRITABLE_INDEXES, REGISTRATION_METHODS } from './token-admin-registry/constants.ts' export type { TransactionResult } from '../operation.ts' export type { SerializedSolanaTxEncoding } from './serialize.ts' export type * from './token/operations/index.ts' diff --git a/ccip-sdk/src/cct/solana/token-admin-registry/constants.ts b/ccip-sdk/src/cct/solana/token-admin-registry/constants.ts new file mode 100644 index 000000000..93341423e --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/constants.ts @@ -0,0 +1,11 @@ +/** Authorization paths used to register a token in the TokenAdminRegistry. */ +export const REGISTRATION_METHODS = { + OWNER: 'owner', + CCIP_ADMIN: 'ccip-admin', +} as const + +/** + * Positions of `poolConfig` (3), `poolTokenAta` (4), and `tokenMint` (7) in the pool ALT built + * by `createLookupTable`. Custom pools must extend this, e.g. `[...DEFAULT_WRITABLE_INDEXES, n]`. + */ +export const DEFAULT_WRITABLE_INDEXES = [3, 4, 7] as const 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 index 3df931738..8e85a8a55 100644 --- 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 @@ -19,16 +19,10 @@ import { } 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 +import { REGISTRATION_METHODS } from '../constants.ts' /** Authorization path used to register a token in the TokenAdminRegistry. */ -export type RegisterAdminMethod = - (typeof REGISTER_ADMIN_METHODS)[keyof typeof REGISTER_ADMIN_METHODS] +export type RegisterAdminMethod = (typeof REGISTRATION_METHODS)[keyof typeof REGISTRATION_METHODS] type RegisterAdminParams = { /** Token mint to register. The proposed administrator remains pending until accepted. */ @@ -146,7 +140,7 @@ export class RegisterAdmin extends SolanaOperation< protected override parse(params: GenerateRegisterAdminParams): ParsedRegisterAdminParams { if ( params.registrationMethod !== undefined && - !Object.values(REGISTER_ADMIN_METHODS).includes(params.registrationMethod) + !Object.values(REGISTRATION_METHODS).includes(params.registrationMethod) ) { throw new CCTParamsInvalidError( this.name, @@ -166,7 +160,7 @@ export class RegisterAdmin extends SolanaOperation< ...(params.administrator !== undefined && { administrator: parsePublicKey(this.name, 'administrator', params.administrator), }), - method: params.registrationMethod ?? REGISTER_ADMIN_METHODS.OWNER, + method: params.registrationMethod ?? REGISTRATION_METHODS.OWNER, } } @@ -204,12 +198,12 @@ export class RegisterAdmin extends SolanaOperation< const instructions: TransactionInstruction[] = [] switch (method) { - case REGISTER_ADMIN_METHODS.OWNER: { + case REGISTRATION_METHODS.OWNER: { const ownerIx = await buildOwnerInstruction(program, accounts, mintAuthority, administrator) instructions.push(ownerIx) break } - case REGISTER_ADMIN_METHODS.CCIP_ADMIN: { + case REGISTRATION_METHODS.CCIP_ADMIN: { const ccipAdminIx = await buildCcipAdminInstruction(program, accounts, administrator) instructions.push(ccipAdminIx) break 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 index 8a44bea77..f2bf50c0b 100644 --- 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 @@ -7,7 +7,7 @@ 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' +import { DEFAULT_WRITABLE_INDEXES, SolanaTokenManager } from '../../index.ts' const BLOCKHASH = PublicKey.default.toBase58() const TOKEN = Keypair.generate().publicKey.toBase58() @@ -58,6 +58,12 @@ describe('SetPool (cct/solana)', () => { assert.ok(instruction.keys.some((key) => key.pubkey.toBase58() === PAYER)) }) + it('accepts the default writable indexes directly', async () => { + const unsigned = await generate({ writableIndexes: DEFAULT_WRITABLE_INDEXES }) + + assert.equal(unsigned.instructions[0]!.data.toString('hex'), '771e0eb473e1a7ee03000000030407') + }) + it('uses caller-provided writable indexes', async () => { const unsigned = await generate({ writableIndexes: [3, 4, 7, 9] }) 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 index bfcb5a0f5..40d649308 100644 --- 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 @@ -17,9 +17,7 @@ import { 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 +import { DEFAULT_WRITABLE_INDEXES } from '../constants.ts' /** Parameters shared by Solana TokenAdminRegistry `setPool` generation and execution. */ type SetPoolParams = { @@ -37,7 +35,7 @@ type SetPoolParams = { * 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[] + writableIndexes?: readonly number[] /** * Token admin authority. Defaults to `payer` for single-signer transactions. * Multisig/Squads flows should pass the admin/vault authority explicitly. @@ -87,7 +85,7 @@ export class SetPool extends SolanaOperation Date: Thu, 27 Aug 2026 11:00:01 +0800 Subject: [PATCH 70/87] feat(cct-sdk): Add set can accept liquidity op solana (#369) * feat: add transfer pool ownership op solana * feat: add accept pool ownership op solana * feat: add transfer authority op solana * fix: update export barrel * feat: add mint tokens op solana * fix: address comments * fix: address comments * feat: add set can accept liquidity op solana * fix: add lock release token pool idl * fix: update tsdoc * fix: address comments * feat(cct-sdk): Add set rebalancer op solana (#370) * feat: add set rebalancer op solana * fix: update tsdoc * fix: address comments --- ccip-sdk/src/cct/solana/index.test.ts | 4 + ccip-sdk/src/cct/solana/index.ts | 163 ++ .../src/cct/solana/programs/token-pool.ts | 12 +- .../token-admin-registry/operations/index.ts | 9 +- .../cct/solana/token-pool/operations/index.ts | 2 + .../set-can-accept-liquidity.test.ts | 153 ++ .../operations/set-can-accept-liquidity.ts | 125 ++ .../operations/set-rebalancer.test.ts | 156 ++ .../token-pool/operations/set-rebalancer.ts | 118 + ccip-sdk/src/cct/solana/validate.ts | 16 + .../idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts | 1972 +++++++++++++++++ ccip-sdk/src/solana/idl/token-pool-coder.ts | 28 +- 12 files changed, 2748 insertions(+), 10 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/set-rebalancer.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/set-rebalancer.ts create mode 100644 ccip-sdk/src/solana/idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 4b3aa6202..2796eb16a 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -69,10 +69,14 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(typeof cct.deployTokenPool, 'function') assert.equal(typeof cct.generateUnsignedDeleteChainRemoteConfig, 'function') assert.equal(typeof cct.deleteChainRemoteConfig, 'function') + assert.equal(typeof cct.generateUnsignedSetCanAcceptLiquidity, 'function') + assert.equal(typeof cct.setCanAcceptLiquidity, '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.generateUnsignedSetRebalancer, 'function') + assert.equal(typeof cct.setRebalancer, 'function') assert.equal(typeof cct.generateUnsignedTransferOwnership, 'function') assert.equal(typeof cct.transferOwnership, 'function') assert.equal(typeof cct.generateUnsignedAcceptOwnership, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index dda7ba2cf..a76a0d66a 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -94,10 +94,14 @@ import { type ExecuteInitChainRemoteConfigResult, type ExecuteRemoveFromAllowlistParams, type ExecuteRemoveFromAllowlistResult, + type ExecuteSetCanAcceptLiquidityParams, + type ExecuteSetCanAcceptLiquidityResult, type ExecuteSetChainRateLimitParams, type ExecuteSetChainRateLimitResult, type ExecuteSetRateLimitAdminParams, type ExecuteSetRateLimitAdminResult, + type ExecuteSetRebalancerParams, + type ExecuteSetRebalancerResult, type ExecuteTransferOwnershipParams, type ExecuteTransferOwnershipResult, type GenerateAcceptOwnershipParams, @@ -120,10 +124,14 @@ import { type GenerateInitChainRemoteConfigResult, type GenerateRemoveFromAllowlistParams, type GenerateRemoveFromAllowlistResult, + type GenerateSetCanAcceptLiquidityParams, + type GenerateSetCanAcceptLiquidityResult, type GenerateSetChainRateLimitParams, type GenerateSetChainRateLimitResult, type GenerateSetRateLimitAdminParams, type GenerateSetRateLimitAdminResult, + type GenerateSetRebalancerParams, + type GenerateSetRebalancerResult, type GenerateTransferOwnershipParams, type GenerateTransferOwnershipResult, type GetTokenPoolRemotesParams, @@ -144,8 +152,10 @@ import { GetTokenPoolState, InitChainRemoteConfig, RemoveFromAllowlist, + SetCanAcceptLiquidity, SetChainRateLimit, SetRateLimitAdmin, + SetRebalancer, TransferOwnership, } from './token-pool/operations/index.ts' @@ -180,8 +190,10 @@ export class SolanaTokenManager extends TokenManager readonly #getTokenPoolState = new GetTokenPoolState() readonly #initChainRemoteConfig = new InitChainRemoteConfig() readonly #removeFromAllowlist = new RemoveFromAllowlist() + readonly #setCanAcceptLiquidity = new SetCanAcceptLiquidity() readonly #setChainRateLimit = new SetChainRateLimit() readonly #setRateLimitAdmin = new SetRateLimitAdmin() + readonly #setRebalancer = new SetRebalancer() readonly #transferOwnership = new TransferOwnership() /** Creates a Solana CCT manager for an existing chain. */ @@ -1058,6 +1070,157 @@ export class SolanaTokenManager extends TokenManager return this.#setRateLimitAdmin.execute(this.chain, opts) } + /** + * Builds an unsigned instruction that sets whether an initialized Solana lock-release token pool + * accepts `provideLiquidity` deposits and `withdrawLiquidity` transfers. Pass canonical + * `poolType: 'lock-release'` or a compatible `poolProgramAddress`; `authority` defaults to `payer`. + * + * @remarks + * ⚠️ **Consequence:** Setting `allow` to `true` lets the rebalancer both `provideLiquidity` and + * `withdrawLiquidity`. Setting `allow` to `false` **disables both** — liquidity already in the pool cannot be + * withdrawn until `allow` is re-enabled. Verify the current liquidity balance before flipping to `false`. + * + * @see {@link setCanAcceptLiquidity} + * @see {@link generateUnsignedSetRebalancer} + * + * @throws {@link CCTParamsInvalidError} If `allow`, a pool parameter, or public key is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedSetCanAcceptLiquidity({ + * tokenAddress: mint, + * poolType: 'lock-release', + * allow: true, + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedSetCanAcceptLiquidity( + opts: GenerateSetCanAcceptLiquidityParams, + ): Promise { + return this.#setCanAcceptLiquidity.generate(this.chain, opts) + } + + /** + * Sets whether an initialized Solana lock-release token pool accepts `provideLiquidity` deposits + * and `withdrawLiquidity` transfers using the pool owner wallet. + * + * @remarks + * ⚠️ **Consequence:** Setting `allow` to `true` lets the rebalancer both `provideLiquidity` and + * `withdrawLiquidity`. Setting `allow` to `false` **disables both** — liquidity already in the pool cannot be + * withdrawn until `allow` is re-enabled. Verify the current liquidity balance before flipping to `false`. + * + * @see {@link generateUnsignedSetCanAcceptLiquidity} + * @see {@link setRebalancer} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If `allow` or a pool parameter is invalid, or the authority differs + * from the executing wallet. + * @throws {@link CCTTxFailedError} If the wallet is not the pool owner or simulation/submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.setCanAcceptLiquidity({ + * tokenAddress: mint, + * poolType: 'lock-release', + * allow: true, + * wallet, + * }) + * ``` + */ + setCanAcceptLiquidity( + opts: ExecuteSetCanAcceptLiquidityParams, + ): Promise { + return this.#setCanAcceptLiquidity.execute(this.chain, opts) + } + + /** + * Builds an unsigned instruction that sets the address authorized to provide or withdraw + * liquidity for an initialized Solana lock-release token pool. Pass canonical + * `poolType: 'lock-release'` or a compatible `poolProgramAddress`; `authority` defaults to + * `payer`. The default/zero public key (`11111111111111111111111111111111`) disables + * rebalancing. + * + * @remarks + * ⚠️ **Consequence:** Rebalancer is the address allowed to provide or withdraw liquidity. + * Setting the zero address (`11111111111111111111111111111111`) removes the rebalancer; until a new one + * is set, **no account can provide or withdraw liquidity**, even liquidity already in the pool. + * This does not affect whether the pool accepts liquidity — see {@link setCanAcceptLiquidity}. + * + * @see {@link setRebalancer} + * @see {@link setCanAcceptLiquidity} + * @see {@link generateUnsignedSetCanAcceptLiquidity} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter or public key is invalid. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedSetRebalancer({ + * tokenAddress: mint, + * poolType: 'lock-release', + * rebalancer, + * payer, + * authority, + * }) + * ``` + */ + generateUnsignedSetRebalancer( + opts: GenerateSetRebalancerParams, + ): Promise { + return this.#setRebalancer.generate(this.chain, opts) + } + + /** + * Sets the address authorized to provide or withdraw liquidity for an initialized Solana + * lock-release token pool using the pool owner wallet. Pass canonical `poolType: 'lock-release'` + * or a compatible `poolProgramAddress`; set `rebalancer` to the default/zero public key + * (`11111111111111111111111111111111`) to disable rebalancing. + * + * @remarks + * ⚠️ **Consequence:** Rebalancer is the address allowed to provide or withdraw liquidity. + * Setting the zero address (`11111111111111111111111111111111`) removes the rebalancer; until a new one + * is set, **no account can provide or withdraw liquidity**, even liquidity already in the pool. + * This does not affect whether the pool accepts liquidity — see {@link setCanAcceptLiquidity}. + * + * @see {@link generateUnsignedSetRebalancer} + * @see {@link setCanAcceptLiquidity} + * @see {@link generateUnsignedSetCanAcceptLiquidity} + * + * @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 wallet is not the pool owner or simulation/submission fails. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.setRebalancer({ + * tokenAddress: mint, + * poolType: 'lock-release', + * rebalancer, + * wallet, + * }) + * ``` + * + * @example Disable rebalancing + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.setRebalancer({ + * tokenAddress: mint, + * poolType: 'lock-release', + * rebalancer: PublicKey.default.toBase58(), // disable + * wallet, + * }) + * ``` + */ + setRebalancer(opts: ExecuteSetRebalancerParams): Promise { + return this.#setRebalancer.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`. diff --git a/ccip-sdk/src/cct/solana/programs/token-pool.ts b/ccip-sdk/src/cct/solana/programs/token-pool.ts index 9cad1176b..9437c2ffe 100644 --- a/ccip-sdk/src/cct/solana/programs/token-pool.ts +++ b/ccip-sdk/src/cct/solana/programs/token-pool.ts @@ -6,6 +6,7 @@ import { PublicKey } from '@solana/web3.js' import { CCIPError } from '../../../errors/index.ts' import { type TokenPoolConfig, + LOCK_RELEASE_TOKEN_POOL_IDL, TOKEN_POOL_IDL, tokenPoolCoder, } from '../../../solana/idl/token-pool-coder.ts' @@ -64,7 +65,7 @@ export function resolveTokenPoolProgram(poolType: TokenPoolType): PublicKey { return new PublicKey(TOKEN_POOL_PROGRAMS[poolType]) } -/** Creates an Anchor Program client for a token pool program. */ +/** Creates an Anchor Program client for a burn-mint token pool program. */ export function createTokenPoolProgram( chain: SolanaChain, poolProgram: PublicKey, @@ -73,6 +74,15 @@ export function createTokenPoolProgram( return new Program(TOKEN_POOL_IDL, poolProgram, simulationProvider(chain, payer)) } +/** Creates an Anchor Program client for a lock-release token pool program. */ +export function createLockReleaseTokenPoolProgram( + chain: SolanaChain, + poolProgram: PublicKey, + payer: PublicKey, +) { + return new Program(LOCK_RELEASE_TOKEN_POOL_IDL, poolProgram, simulationProvider(chain, payer)) +} + /** Decodes a canonical token pool state account. */ export function decodeTokenPoolState( data: Buffer, 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 index 0437d089f..303f5e54b 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -3,6 +3,13 @@ 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 { RegisterAdmin } from './register-admin.ts' +export type { + ExecuteRegisterAdminParams, + ExecuteRegisterAdminResult, + GenerateRegisterAdminParams, + GenerateRegisterAdminResult, + RegisterAdminMethod, +} from './register-admin.ts' export * from './set-pool.ts' export * from './transfer-admin.ts' diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index c01db6416..7ac19b448 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -10,6 +10,8 @@ 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-can-accept-liquidity.ts' export * from './set-chain-rate-limit.ts' export * from './set-rate-limit-admin.ts' +export * from './set-rebalancer.ts' export * from './transfer-ownership.ts' diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.test.ts new file mode 100644 index 000000000..0d58b62a6 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.test.ts @@ -0,0 +1,153 @@ +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 { lockReleaseTokenPoolCoder } 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 ALLOW = true +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()).generateUnsignedSetCanAcceptLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + payer: PAYER, + authority: AUTHORITY, + allow: ALLOW, + ...opts, + }) +} + +describe('SetCanAcceptLiquidity (cct/solana)', () => { + describe('generate', () => { + it('builds the set-can-accept-liquidity instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('lock-release') + const decoded = lockReleaseTokenPoolCoder.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, 'setCanAcceptLiquidity') + assert.equal((decoded.data as { allow: boolean }).allow, ALLOW) + }) + + 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, non-boolean values, and burn-mint pools', async () => { + for (const [opts, param] of [ + [{ tokenAddress: 'invalid' }, 'tokenAddress'], + [{ allow: 'true' }, 'allow'], + [{ poolType: 'burn-mint' as const }, 'poolType'], + [ + { + poolType: undefined, + poolProgramAddress: resolveTokenPoolProgram('burn-mint').toBase58(), + }, + 'poolProgramAddress', + ], + ]) { + 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()).setCanAcceptLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + allow: ALLOW, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed configuration', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).setCanAcceptLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + allow: ALLOW, + authority: AUTHORITY, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setCanAcceptLiquidity' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.ts b/ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.ts new file mode 100644 index 000000000..0bbd6ac6f --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.ts @@ -0,0 +1,125 @@ +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 { CCTParamsInvalidError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type CustomPoolProgramRef, + type LockReleasePoolProgramRef, + createLockReleaseTokenPoolProgram, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parsePublicKey, + resolveLockReleasePoolProgram, + validateAuthorityMatchesWallet, +} from '../../validate.ts' + +/** Parameters shared by Solana lock-release pool liquidity-acceptance generation and execution. */ +type SetCanAcceptLiquidityParams = (LockReleasePoolProgramRef | CustomPoolProgramRef) & { + /** Token mint address managed by the pool. */ + tokenAddress: string + /** Whether to enable liquidity provision and withdrawal. */ + allow: boolean + /** Pool owner. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedSetCanAcceptLiquidityParams = { + tokenAddress: PublicKey + poolProgram: PublicKey + allow: boolean + payer: PublicKey + authority: PublicKey +} + +/** Parameters for unsigned Solana lock-release pool liquidity-acceptance configuration. */ +export type GenerateSetCanAcceptLiquidityParams = SolanaGenerateParams + +/** Unsigned Solana lock-release pool liquidity-acceptance configuration result. */ +export type GenerateSetCanAcceptLiquidityResult = UnsignedSolanaTx + +/** Parameters for executing Solana lock-release pool liquidity-acceptance configuration. */ +export type ExecuteSetCanAcceptLiquidityParams = SolanaExecuteParams + +/** Result of executing Solana lock-release pool liquidity-acceptance configuration. */ +export type ExecuteSetCanAcceptLiquidityResult = TransactionResult + +/** Sets whether a Solana lock-release token pool accepts liquidity. */ +export class SetCanAcceptLiquidity extends SolanaOperation< + SetCanAcceptLiquidityParams, + UnsignedSolanaTx, + ParsedSetCanAcceptLiquidityParams +> { + readonly name = 'setCanAcceptLiquidity' + + /** Parses public keys, validates `allow`, and defaults authority to payer. */ + protected override parse( + params: GenerateSetCanAcceptLiquidityParams, + ): ParsedSetCanAcceptLiquidityParams { + if (typeof params.allow !== 'boolean') { + throw new CCTParamsInvalidError(this.name, 'allow', 'must be a boolean') + } + + const poolProgram = resolveLockReleasePoolProgram(this.name, params) + const payer = parsePublicKey(this.name, 'payer', params.payer) + + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + poolProgram, + allow: params.allow, + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + } + } + + /** Builds the unsigned Solana `setCanAcceptLiquidity` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedSetCanAcceptLiquidityParams, + ): Promise { + const instruction = await createLockReleaseTokenPoolProgram(chain, opts.poolProgram, opts.payer) + .methods.setCanAcceptLiquidity(opts.allow) + .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 pool owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteSetCanAcceptLiquidityParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'setCanAcceptLiquidity requires authority to be the executing wallet. Use generateUnsignedSetCanAcceptLiquidity 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-rebalancer.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/set-rebalancer.test.ts new file mode 100644 index 000000000..d16399c9e --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/set-rebalancer.test.ts @@ -0,0 +1,156 @@ +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 { lockReleaseTokenPoolCoder } 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 REBALANCER = 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()).generateUnsignedSetRebalancer({ + tokenAddress: TOKEN, + poolType: 'lock-release', + payer: PAYER, + authority: AUTHORITY, + rebalancer: REBALANCER, + ...opts, + }) +} + +describe('SetRebalancer (cct/solana)', () => { + describe('generate', () => { + it('builds the set-rebalancer instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('lock-release') + const decoded = lockReleaseTokenPoolCoder.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, 'setRebalancer') + assert.equal((decoded.data as { rebalancer: PublicKey }).rebalancer.toBase58(), REBALANCER) + }) + + it('defaults authority to payer and accepts the default rebalancer', async () => { + const unsigned = await generate({ + authority: undefined, + rebalancer: PublicKey.default.toBase58(), + }) + + 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 and burn-mint pools', async () => { + for (const [opts, param] of [ + [{ tokenAddress: 'invalid' }, 'tokenAddress'], + [{ rebalancer: 'invalid' }, 'rebalancer'], + [{ poolType: 'burn-mint' as const }, 'poolType'], + [ + { + poolType: undefined, + poolProgramAddress: resolveTokenPoolProgram('burn-mint').toBase58(), + }, + 'poolProgramAddress', + ], + ]) { + 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()).setRebalancer({ + tokenAddress: TOKEN, + poolType: 'lock-release', + rebalancer: REBALANCER, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed configuration', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).setRebalancer({ + tokenAddress: TOKEN, + poolType: 'lock-release', + rebalancer: REBALANCER, + authority: AUTHORITY, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setRebalancer' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/set-rebalancer.ts b/ccip-sdk/src/cct/solana/token-pool/operations/set-rebalancer.ts new file mode 100644 index 000000000..d7a8640ff --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/set-rebalancer.ts @@ -0,0 +1,118 @@ +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 CustomPoolProgramRef, + type LockReleasePoolProgramRef, + createLockReleaseTokenPoolProgram, + deriveTokenPoolConfigPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + parsePublicKey, + resolveLockReleasePoolProgram, + validateAuthorityMatchesWallet, +} from '../../validate.ts' + +/** Parameters shared by Solana lock-release pool rebalancer generation and execution. */ +type SetRebalancerParams = (LockReleasePoolProgramRef | CustomPoolProgramRef) & { + /** Token mint address managed by the pool. */ + tokenAddress: string + /** Address authorized to provide or withdraw pool liquidity (stored on the pool; not a transaction signer). Use the default/zero address (`11111111111111111111111111111111`) to disable rebalancing. */ + rebalancer: string + /** Pool owner. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedSetRebalancerParams = { + tokenAddress: PublicKey + rebalancer: PublicKey + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey +} + +/** Parameters for unsigned Solana lock-release pool rebalancer configuration. */ +export type GenerateSetRebalancerParams = SolanaGenerateParams + +/** Unsigned Solana lock-release pool rebalancer configuration result. */ +export type GenerateSetRebalancerResult = UnsignedSolanaTx + +/** Parameters for executing Solana lock-release pool rebalancer configuration. */ +export type ExecuteSetRebalancerParams = SolanaExecuteParams + +/** Result of executing Solana lock-release pool rebalancer configuration. */ +export type ExecuteSetRebalancerResult = TransactionResult + +/** Sets the address authorized to provide or withdraw a Solana lock-release token pool's liquidity. */ +export class SetRebalancer extends SolanaOperation< + SetRebalancerParams, + UnsignedSolanaTx, + ParsedSetRebalancerParams +> { + readonly name = 'setRebalancer' + + /** Parses public keys and defaults authority to payer without mutating caller params. */ + protected override parse(params: GenerateSetRebalancerParams): ParsedSetRebalancerParams { + const poolProgram = resolveLockReleasePoolProgram(this.name, params) + const payer = parsePublicKey(this.name, 'payer', params.payer) + + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + rebalancer: parsePublicKey(this.name, 'rebalancer', params.rebalancer), + poolProgram, + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + } + } + + /** Builds the unsigned Solana `setRebalancer` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedSetRebalancerParams, + ): Promise { + const instruction = await createLockReleaseTokenPoolProgram(chain, opts.poolProgram, opts.payer) + .methods.setRebalancer(opts.rebalancer) + .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 pool owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteSetRebalancerParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'setRebalancer requires authority to be the executing wallet. Use generateUnsignedSetRebalancer for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index f51836d7b..49cb7ef2f 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -135,6 +135,22 @@ export function resolvePoolProgram(operation: string, params: PoolProgramRef): P return parsePublicKey(operation, 'poolProgramAddress', params.poolProgramAddress) } +/** Resolves a lock-release token pool program and rejects the canonical burn-mint program. */ +export function resolveLockReleasePoolProgram( + operation: string, + params: PoolProgramRef, +): PublicKey { + const poolProgram = resolvePoolProgram(operation, params) + if (poolProgram.equals(resolveTokenPoolProgram('burn-mint'))) { + throw new CCTParamsInvalidError( + operation, + params.poolProgramAddress === undefined ? 'poolType' : 'poolProgramAddress', + 'must be lock-release', + ) + } + return poolProgram +} + /** * Asserts `value` is an integer, optionally inside inclusive bounds. * @throws CCTParamsInvalidError if `value` is not an integer or is outside bounds. diff --git a/ccip-sdk/src/solana/idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts b/ccip-sdk/src/solana/idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts new file mode 100644 index 000000000..29cf17a06 --- /dev/null +++ b/ccip-sdk/src/solana/idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts @@ -0,0 +1,1972 @@ +// generate: +// fetch('https://raw.githubusercontent.com/smartcontractkit/chainlink-ccip/refs/heads/main/chains/solana/contracts/target/types/lockrelease_token_pool.ts') +// .then((res) => res.text()) +// .then((text) => text.trim()) +export type LockreleaseTokenPool = { + version: '1.6.3' + name: 'lockrelease_token_pool' + instructions: [ + { + name: 'initGlobalConfig' + accounts: [ + { + name: 'config' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'routerAddress' + type: 'publicKey' + }, + { + name: 'rmnAddress' + type: 'publicKey' + }, + ] + }, + { + name: 'updateSelfServedAllowed' + accounts: [ + { + name: 'config' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'selfServedAllowed' + type: 'bool' + }, + ] + }, + { + name: 'updateDefaultRouter' + accounts: [ + { + name: 'config' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'routerAddress' + type: 'publicKey' + }, + ] + }, + { + name: 'updateDefaultRmn' + accounts: [ + { + name: 'config' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'rmnAddress' + type: 'publicKey' + }, + ] + }, + { + name: 'initialize' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + { + name: 'config' + isMut: false + isSigner: false + }, + ] + args: [] + }, + { + name: 'typeVersion' + docs: [ + 'Returns the program type (name) and version.', + 'Used by offchain code to easily determine which program & version is being interacted with.', + '', + '# Arguments', + '* `ctx` - The context', + ] + accounts: [ + { + name: 'clock' + isMut: false + isSigner: false + }, + ] + args: [] + returns: 'string' + }, + { + name: 'transferOwnership' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [ + { + name: 'proposedOwner' + type: 'publicKey' + }, + ] + }, + { + name: 'acceptOwnership' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [] + }, + { + name: 'setRouter' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'newRouter' + type: 'publicKey' + }, + ] + }, + { + name: 'setRmn' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'program' + isMut: false + isSigner: false + }, + { + name: 'programData' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'rmnAddress' + type: 'publicKey' + }, + ] + }, + { + name: 'initializeStateVersion' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + ] + args: [ + { + name: 'mint' + type: 'publicKey' + }, + ] + }, + { + name: 'initChainRemoteConfig' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'remoteChainSelector' + type: 'u64' + }, + { + name: 'mint' + type: 'publicKey' + }, + { + name: 'cfg' + type: { + defined: 'RemoteConfig' + } + }, + ] + }, + { + name: 'editChainRemoteConfig' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'remoteChainSelector' + type: 'u64' + }, + { + name: 'mint' + type: 'publicKey' + }, + { + name: 'cfg' + type: { + defined: 'RemoteConfig' + } + }, + ] + }, + { + name: 'appendRemotePoolAddresses' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'remoteChainSelector' + type: 'u64' + }, + { + name: 'mint' + type: 'publicKey' + }, + { + name: 'addresses' + type: { + vec: { + defined: 'RemoteAddress' + } + } + }, + ] + }, + { + name: 'setChainRateLimit' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + ] + args: [ + { + name: 'remoteChainSelector' + type: 'u64' + }, + { + name: 'mint' + type: 'publicKey' + }, + { + name: 'inbound' + type: { + defined: 'RateLimitConfig' + } + }, + { + name: 'outbound' + type: { + defined: 'RateLimitConfig' + } + }, + ] + }, + { + name: 'setRateLimitAdmin' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + ] + args: [ + { + name: 'mint' + type: 'publicKey' + }, + { + name: 'newRateLimitAdmin' + type: 'publicKey' + }, + ] + }, + { + name: 'deleteChainConfig' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + ] + args: [ + { + name: 'remoteChainSelector' + type: 'u64' + }, + { + name: 'mint' + type: 'publicKey' + }, + ] + }, + { + name: 'configureAllowList' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'add' + type: { + vec: 'publicKey' + } + }, + { + name: 'enabled' + type: 'bool' + }, + ] + }, + { + name: 'removeFromAllowList' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: true + isSigner: true + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + ] + args: [ + { + name: 'remove' + type: { + vec: 'publicKey' + } + }, + ] + }, + { + name: 'releaseOrMintTokens' + accounts: [ + { + name: 'authority' + isMut: false + isSigner: true + }, + { + name: 'offrampProgram' + isMut: false + isSigner: false + docs: [ + 'CHECK offramp program: exists only to derive the allowed offramp PDA', + 'and the authority PDA.', + ] + }, + { + name: 'allowedOfframp' + isMut: false + isSigner: false + docs: [ + 'CHECK PDA of the router program verifying the signer is an allowed offramp.', + "If PDA does not exist, the router doesn't allow this offramp", + ] + }, + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'tokenProgram' + isMut: false + isSigner: false + }, + { + name: 'mint' + isMut: true + isSigner: false + }, + { + name: 'poolSigner' + isMut: false + isSigner: false + }, + { + name: 'poolTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + { + name: 'rmnRemote' + isMut: false + isSigner: false + }, + { + name: 'rmnRemoteCurses' + isMut: false + isSigner: false + }, + { + name: 'rmnRemoteConfig' + isMut: false + isSigner: false + }, + { + name: 'receiverTokenAccount' + isMut: true + isSigner: false + }, + ] + args: [ + { + name: 'releaseOrMint' + type: { + defined: 'ReleaseOrMintInV1' + } + }, + ] + returns: { + defined: 'ReleaseOrMintOutV1' + } + }, + { + name: 'lockOrBurnTokens' + accounts: [ + { + name: 'authority' + isMut: false + isSigner: true + }, + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'tokenProgram' + isMut: false + isSigner: false + }, + { + name: 'mint' + isMut: true + isSigner: false + }, + { + name: 'poolSigner' + isMut: false + isSigner: false + }, + { + name: 'poolTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'rmnRemote' + isMut: false + isSigner: false + }, + { + name: 'rmnRemoteCurses' + isMut: false + isSigner: false + }, + { + name: 'rmnRemoteConfig' + isMut: false + isSigner: false + }, + { + name: 'chainConfig' + isMut: true + isSigner: false + }, + ] + args: [ + { + name: 'lockOrBurn' + type: { + defined: 'LockOrBurnInV1' + } + }, + ] + returns: { + defined: 'LockOrBurnOutV1' + } + }, + { + name: 'setRebalancer' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [ + { + name: 'rebalancer' + type: 'publicKey' + }, + ] + }, + { + name: 'setCanAcceptLiquidity' + accounts: [ + { + name: 'state' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [ + { + name: 'allow' + type: 'bool' + }, + ] + }, + { + name: 'provideLiquidity' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'tokenProgram' + isMut: false + isSigner: false + }, + { + name: 'mint' + isMut: true + isSigner: false + }, + { + name: 'poolSigner' + isMut: false + isSigner: false + }, + { + name: 'poolTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'remoteTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [ + { + name: 'amount' + type: 'u64' + }, + ] + }, + { + name: 'withdrawLiquidity' + accounts: [ + { + name: 'state' + isMut: false + isSigner: false + }, + { + name: 'tokenProgram' + isMut: false + isSigner: false + }, + { + name: 'mint' + isMut: true + isSigner: false + }, + { + name: 'poolSigner' + isMut: false + isSigner: false + }, + { + name: 'poolTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'remoteTokenAccount' + isMut: true + isSigner: false + }, + { + name: 'authority' + isMut: false + isSigner: true + }, + ] + args: [ + { + name: 'amount' + type: 'u64' + }, + ] + }, + ] + accounts: [ + { + name: 'poolConfig' + type: { + kind: 'struct' + fields: [ + { + name: 'version' + type: 'u8' + }, + { + name: 'selfServedAllowed' + type: 'bool' + }, + { + name: 'router' + type: 'publicKey' + }, + { + name: 'rmnRemote' + type: 'publicKey' + }, + ] + } + }, + { + name: 'state' + type: { + kind: 'struct' + fields: [ + { + name: 'version' + type: 'u8' + }, + { + name: 'config' + type: { + defined: 'BaseConfig' + } + }, + ] + } + }, + { + name: 'chainConfig' + type: { + kind: 'struct' + fields: [ + { + name: 'base' + type: { + defined: 'BaseChain' + } + }, + ] + } + }, + ] +} + +export const IDL: LockreleaseTokenPool = { + version: '1.6.3', + name: 'lockrelease_token_pool', + instructions: [ + { + name: 'initGlobalConfig', + accounts: [ + { + name: 'config', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'routerAddress', + type: 'publicKey', + }, + { + name: 'rmnAddress', + type: 'publicKey', + }, + ], + }, + { + name: 'updateSelfServedAllowed', + accounts: [ + { + name: 'config', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'selfServedAllowed', + type: 'bool', + }, + ], + }, + { + name: 'updateDefaultRouter', + accounts: [ + { + name: 'config', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'routerAddress', + type: 'publicKey', + }, + ], + }, + { + name: 'updateDefaultRmn', + accounts: [ + { + name: 'config', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'rmnAddress', + type: 'publicKey', + }, + ], + }, + { + name: 'initialize', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + { + name: 'config', + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + { + name: 'typeVersion', + docs: [ + 'Returns the program type (name) and version.', + 'Used by offchain code to easily determine which program & version is being interacted with.', + '', + '# Arguments', + '* `ctx` - The context', + ], + accounts: [ + { + name: 'clock', + isMut: false, + isSigner: false, + }, + ], + args: [], + returns: 'string', + }, + { + name: 'transferOwnership', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [ + { + name: 'proposedOwner', + type: 'publicKey', + }, + ], + }, + { + name: 'acceptOwnership', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [], + }, + { + name: 'setRouter', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'newRouter', + type: 'publicKey', + }, + ], + }, + { + name: 'setRmn', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'program', + isMut: false, + isSigner: false, + }, + { + name: 'programData', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'rmnAddress', + type: 'publicKey', + }, + ], + }, + { + name: 'initializeStateVersion', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + ], + args: [ + { + name: 'mint', + type: 'publicKey', + }, + ], + }, + { + name: 'initChainRemoteConfig', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'remoteChainSelector', + type: 'u64', + }, + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'cfg', + type: { + defined: 'RemoteConfig', + }, + }, + ], + }, + { + name: 'editChainRemoteConfig', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'remoteChainSelector', + type: 'u64', + }, + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'cfg', + type: { + defined: 'RemoteConfig', + }, + }, + ], + }, + { + name: 'appendRemotePoolAddresses', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'remoteChainSelector', + type: 'u64', + }, + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'addresses', + type: { + vec: { + defined: 'RemoteAddress', + }, + }, + }, + ], + }, + { + name: 'setChainRateLimit', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + ], + args: [ + { + name: 'remoteChainSelector', + type: 'u64', + }, + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'inbound', + type: { + defined: 'RateLimitConfig', + }, + }, + { + name: 'outbound', + type: { + defined: 'RateLimitConfig', + }, + }, + ], + }, + { + name: 'setRateLimitAdmin', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + ], + args: [ + { + name: 'mint', + type: 'publicKey', + }, + { + name: 'newRateLimitAdmin', + type: 'publicKey', + }, + ], + }, + { + name: 'deleteChainConfig', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + ], + args: [ + { + name: 'remoteChainSelector', + type: 'u64', + }, + { + name: 'mint', + type: 'publicKey', + }, + ], + }, + { + name: 'configureAllowList', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'add', + type: { + vec: 'publicKey', + }, + }, + { + name: 'enabled', + type: 'bool', + }, + ], + }, + { + name: 'removeFromAllowList', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: true, + isSigner: true, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'remove', + type: { + vec: 'publicKey', + }, + }, + ], + }, + { + name: 'releaseOrMintTokens', + accounts: [ + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'offrampProgram', + isMut: false, + isSigner: false, + docs: [ + 'CHECK offramp program: exists only to derive the allowed offramp PDA', + 'and the authority PDA.', + ], + }, + { + name: 'allowedOfframp', + isMut: false, + isSigner: false, + docs: [ + 'CHECK PDA of the router program verifying the signer is an allowed offramp.', + "If PDA does not exist, the router doesn't allow this offramp", + ], + }, + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + { + name: 'mint', + isMut: true, + isSigner: false, + }, + { + name: 'poolSigner', + isMut: false, + isSigner: false, + }, + { + name: 'poolTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + { + name: 'rmnRemote', + isMut: false, + isSigner: false, + }, + { + name: 'rmnRemoteCurses', + isMut: false, + isSigner: false, + }, + { + name: 'rmnRemoteConfig', + isMut: false, + isSigner: false, + }, + { + name: 'receiverTokenAccount', + isMut: true, + isSigner: false, + }, + ], + args: [ + { + name: 'releaseOrMint', + type: { + defined: 'ReleaseOrMintInV1', + }, + }, + ], + returns: { + defined: 'ReleaseOrMintOutV1', + }, + }, + { + name: 'lockOrBurnTokens', + accounts: [ + { + name: 'authority', + isMut: false, + isSigner: true, + }, + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + { + name: 'mint', + isMut: true, + isSigner: false, + }, + { + name: 'poolSigner', + isMut: false, + isSigner: false, + }, + { + name: 'poolTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'rmnRemote', + isMut: false, + isSigner: false, + }, + { + name: 'rmnRemoteCurses', + isMut: false, + isSigner: false, + }, + { + name: 'rmnRemoteConfig', + isMut: false, + isSigner: false, + }, + { + name: 'chainConfig', + isMut: true, + isSigner: false, + }, + ], + args: [ + { + name: 'lockOrBurn', + type: { + defined: 'LockOrBurnInV1', + }, + }, + ], + returns: { + defined: 'LockOrBurnOutV1', + }, + }, + { + name: 'setRebalancer', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [ + { + name: 'rebalancer', + type: 'publicKey', + }, + ], + }, + { + name: 'setCanAcceptLiquidity', + accounts: [ + { + name: 'state', + isMut: true, + isSigner: false, + }, + { + name: 'mint', + isMut: false, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [ + { + name: 'allow', + type: 'bool', + }, + ], + }, + { + name: 'provideLiquidity', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + { + name: 'mint', + isMut: true, + isSigner: false, + }, + { + name: 'poolSigner', + isMut: false, + isSigner: false, + }, + { + name: 'poolTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'remoteTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [ + { + name: 'amount', + type: 'u64', + }, + ], + }, + { + name: 'withdrawLiquidity', + accounts: [ + { + name: 'state', + isMut: false, + isSigner: false, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + { + name: 'mint', + isMut: true, + isSigner: false, + }, + { + name: 'poolSigner', + isMut: false, + isSigner: false, + }, + { + name: 'poolTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'remoteTokenAccount', + isMut: true, + isSigner: false, + }, + { + name: 'authority', + isMut: false, + isSigner: true, + }, + ], + args: [ + { + name: 'amount', + type: 'u64', + }, + ], + }, + ], + accounts: [ + { + name: 'poolConfig', + type: { + kind: 'struct', + fields: [ + { + name: 'version', + type: 'u8', + }, + { + name: 'selfServedAllowed', + type: 'bool', + }, + { + name: 'router', + type: 'publicKey', + }, + { + name: 'rmnRemote', + type: 'publicKey', + }, + ], + }, + }, + { + name: 'state', + type: { + kind: 'struct', + fields: [ + { + name: 'version', + type: 'u8', + }, + { + name: 'config', + type: { + defined: 'BaseConfig', + }, + }, + ], + }, + }, + { + name: 'chainConfig', + type: { + kind: 'struct', + fields: [ + { + name: 'base', + type: { + defined: 'BaseChain', + }, + }, + ], + }, + }, + ], +} +// generate:end diff --git a/ccip-sdk/src/solana/idl/token-pool-coder.ts b/ccip-sdk/src/solana/idl/token-pool-coder.ts index 088dcf96e..3237b517e 100644 --- a/ccip-sdk/src/solana/idl/token-pool-coder.ts +++ b/ccip-sdk/src/solana/idl/token-pool-coder.ts @@ -1,18 +1,30 @@ -import { type IdlTypes, BorshCoder } from '@coral-xyz/anchor' +import { type Idl, 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' +import { IDL as LOCK_RELEASE_TOKEN_POOL } from './1.6.0/LOCK_RELEASE_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], +/** Adds shared base token-pool types, events, and errors to a pool-specific IDL. */ +function composeTokenPoolIdl(poolIdl: T) { + return { + ...poolIdl, + types: BASE_TOKEN_POOL.types, + events: BASE_TOKEN_POOL.events, + errors: [...BASE_TOKEN_POOL.errors, ...(poolIdl.errors ?? [])], + } } +/** Burn-mint token pool IDL with shared base definitions. */ +export const TOKEN_POOL_IDL = composeTokenPoolIdl(BURN_MINT_TOKEN_POOL) + +/** Lock-release token pool IDL with shared base definitions. */ +export const LOCK_RELEASE_TOKEN_POOL_IDL = composeTokenPoolIdl(LOCK_RELEASE_TOKEN_POOL) + /** Shared state configuration stored by canonical Solana token pools. */ export type TokenPoolConfig = IdlTypes['BaseConfig'] -/** Borsh decoder for canonical token pool accounts. */ +/** Borsh decoder for burn-mint token pool instructions and canonical token pool accounts. */ export const tokenPoolCoder = new BorshCoder(TOKEN_POOL_IDL) + +/** Borsh decoder for lock-release token pool instructions and canonical token pool accounts. */ +export const lockReleaseTokenPoolCoder = new BorshCoder(LOCK_RELEASE_TOKEN_POOL_IDL) From 9a077f7b389eeb93e1219e07da9168b32e0c00d7 Mon Sep 17 00:00:00 2001 From: Mervin Date: Thu, 27 Aug 2026 20:44:39 +0800 Subject: [PATCH 71/87] feat(cct-sdk): Add approve token op solana (#376) * feat: add transfer pool ownership op solana * feat: add accept pool ownership op solana * feat: add transfer authority op solana * fix: update export barrel * feat: add mint tokens op solana * fix: address comments * fix: address comments * feat: add set can accept liquidity op solana * fix: add lock release token pool idl * feat: add set rebalancer op solana * fix: update tsdoc * feat: add approve token op solana * fix: address comments * fix: revert unrelated changes --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 72 ++++++ .../token/operations/approve-token.test.ts | 223 ++++++++++++++++++ .../solana/token/operations/approve-token.ts | 151 ++++++++++++ .../src/cct/solana/token/operations/index.ts | 1 + .../solana/token/operations/mint-tokens.ts | 20 +- ccip-sdk/src/cct/solana/validate.ts | 31 ++- 7 files changed, 482 insertions(+), 18 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token/operations/approve-token.test.ts create mode 100644 ccip-sdk/src/cct/solana/token/operations/approve-token.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 2796eb16a..277f021da 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -33,6 +33,8 @@ describe('SolanaTokenManager (cct/solana)', () => { // Token operations assert.equal(typeof cct.generateUnsignedDeployToken, 'function') assert.equal(typeof cct.deployToken, 'function') + assert.equal(typeof cct.generateUnsignedApproveToken, 'function') + assert.equal(typeof cct.approveToken, 'function') assert.equal(typeof cct.generateUnsignedCreateTokenAccount, 'function') assert.equal(typeof cct.createTokenAccount, 'function') assert.equal(typeof cct.generateUnsignedMintTokens, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index a76a0d66a..b2628455c 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -13,6 +13,8 @@ import type { UnsignedSolanaTx } from '../../solana/types.ts' import { TokenManager } from '../token-manager.ts' import { type SerializedSolanaTxEncoding, serializeUnsignedSolanaTx } from './serialize.ts' import { + type ExecuteApproveTokenParams, + type ExecuteApproveTokenResult, type ExecuteCreateTokenAccountParams, type ExecuteCreateTokenAccountResult, type ExecuteDeployTokenParams, @@ -21,6 +23,8 @@ import { type ExecuteMintTokensResult, type ExecuteSetTokenAuthorityParams, type ExecuteSetTokenAuthorityResult, + type GenerateApproveTokenParams, + type GenerateApproveTokenResult, type GenerateCreateTokenAccountParams, type GenerateCreateTokenAccountResult, type GenerateDeployTokenParams, @@ -29,6 +33,7 @@ import { type GenerateMintTokensResult, type GenerateSetTokenAuthorityParams, type GenerateSetTokenAuthorityResult, + ApproveToken, CreateTokenAccount, MintTokens, SetTokenAuthority, @@ -163,6 +168,7 @@ import { export class SolanaTokenManager extends TokenManager { readonly chain: SolanaChain // Token operations + readonly #approveToken = new ApproveToken() readonly #createTokenAccount = new CreateTokenAccount() readonly #mintTokens = new MintTokens() readonly #setTokenAuthority = new SetTokenAuthority() @@ -274,6 +280,72 @@ export class SolanaTokenManager extends TokenManager return new DeployToken().execute(this.chain, opts) } + /** + * Builds unsigned instructions to approve a delegate to transfer SPL tokens. + * + * @see {@link approveToken} For wallet-based execution. + * + * @remarks + * This is a prerequisite for pool liquidity operations: approve the pool signer PDA as `delegate` + * with the maximum allowance it may transfer during `provideLiquidity`. Approval grants a trusted + * delegate spend authority and replaces the account's existing delegate and allowance; set `amount` + * to `0n` to clear the allowance. `tokenAccount` defaults to the authority's existing associated token + * account. For an SPL Token multisig authority, provide `multisigSigners` and collect member signatures + * externally. + * + * @throws {@link CCTParamsInvalidError} If an address, allowance, or multisig signer is invalid. + * @throws {@link CCIPTokenAccountNotFoundError} If the token account does not exist. + * @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.generateUnsignedApproveToken({ + * payer: owner, + * tokenAddress: mint, + * delegate, + * amount: 1_000_000n, + * }) + * ``` + */ + generateUnsignedApproveToken( + opts: GenerateApproveTokenParams, + ): Promise { + return this.#approveToken.generate(this.chain, opts) + } + + /** + * Approves a delegate to transfer SPL tokens from the selected token account using the executing + * authority wallet. + * + * @see {@link generateUnsignedApproveToken} For externally signed transactions. + * + * @remarks + * This is a prerequisite for pool liquidity operations: approve the pool signer PDA as `delegate` + * with the maximum allowance it may transfer during `provideLiquidity`. Approval grants a trusted + * delegate spend authority and replaces the account's existing delegate and allowance; set `amount` + * to `0n` to clear the allowance. `tokenAccount` defaults to the authority's existing associated token + * account. SPL Token multisig authorities require `multisigSigners`. + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address, allowance, or multisig signer is invalid, or + * `authority` does not match the executing wallet. + * @throws {@link CCIPTokenAccountNotFoundError} If the token account does not exist. + * @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 SPL Token program rejects the transaction. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.approveToken({ wallet, tokenAddress: mint, delegate, amount: 1_000_000n }) + * ``` + */ + approveToken(opts: ExecuteApproveTokenParams): Promise { + return this.#approveToken.execute(this.chain, opts) + } + /** * Builds an unsigned idempotent associated token account create instruction. * diff --git a/ccip-sdk/src/cct/solana/token/operations/approve-token.test.ts b/ccip-sdk/src/cct/solana/token/operations/approve-token.test.ts new file mode 100644 index 000000000..8a7061460 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/approve-token.test.ts @@ -0,0 +1,223 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, +} from '@solana/spl-token' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { + CCIPTokenAccountNotFoundError, + CCIPTokenMintInvalidError, + CCIPTokenMintNotFoundError, +} 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' +import { U64_MAX } from '../../validate.ts' + +const TOKEN = Keypair.generate().publicKey +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const DELEGATE = Keypair.generate().publicKey.toBase58() +const TOKEN_ACCOUNT = Keypair.generate().publicKey.toBase58() +const MULTISIG = Keypair.generate().publicKey.toBase58() +const MULTISIG_SIGNER = Keypair.generate().publicKey.toBase58() +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { publicKey: Keypair.generate().publicKey, signTransaction: async (tx: T) => tx } + +function chain( + mintOwner: PublicKey | null = TOKEN_PROGRAM_ID, + tokenAccountExists = true, +): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async (address: PublicKey) => { + if (!mintOwner || address.equals(TOKEN)) return mintOwner ? { owner: mintOwner } : null + return tokenAccountExists ? { owner: mintOwner, data: Buffer.alloc(165) } : null + }, + }, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + ...chain(), + connection: { + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID, data: Buffer.alloc(165) }), + 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: Record = {}, mintOwner?: PublicKey | null) { + return SolanaTokenManager.fromChain(chain(mintOwner)).generateUnsignedApproveToken({ + payer: PAYER, + tokenAddress: TOKEN.toBase58(), + delegate: DELEGATE, + authority: AUTHORITY, + amount: 1_000_000n, + ...opts, + }) +} + +describe('ApproveToken (cct/solana)', () => { + describe('generate', () => { + it('derives the authority ATA and approves the delegate', async () => { + const unsigned = await generate() + const ata = getAssociatedTokenAddressSync( + TOKEN, + new PublicKey(AUTHORITY), + true, + TOKEN_PROGRAM_ID, + ) + const [instruction] = unsigned.instructions + + assert.ok(instruction) + assert.equal(unsigned.family, ChainFamily.Solana) + assert.equal(unsigned.mainIndex, 0) + assert.equal(instruction.programId.toBase58(), TOKEN_PROGRAM_ID.toBase58()) + assert.equal(instruction.data[0], 4) // Approve + assert.equal(instruction.data.readBigUInt64LE(1), 1_000_000n) + assert.deepEqual( + instruction.keys.slice(0, 3).map(({ pubkey, isSigner }) => ({ + pubkey: pubkey.toBase58(), + isSigner, + })), + [ + { pubkey: ata.toBase58(), isSigner: false }, + { pubkey: DELEGATE, isSigner: false }, + { pubkey: AUTHORITY, isSigner: true }, + ], + ) + }) + + it('uses an explicitly supplied token account', async () => { + const unsigned = await generate({ tokenAccount: TOKEN_ACCOUNT }) + assert.equal(unsigned.instructions[0]!.keys[0]!.pubkey.toBase58(), TOKEN_ACCOUNT) + }) + + it('supports Token-2022 multisig authorities', async () => { + const unsigned = await generate( + { authority: MULTISIG, multisigSigners: [MULTISIG_SIGNER] }, + TOKEN_2022_PROGRAM_ID, + ) + const ata = getAssociatedTokenAddressSync( + TOKEN, + new PublicKey(MULTISIG), + true, + TOKEN_2022_PROGRAM_ID, + ) + const [instruction] = unsigned.instructions + + assert.ok(instruction) + assert.equal(instruction.programId.toBase58(), TOKEN_2022_PROGRAM_ID.toBase58()) + assert.equal(instruction.keys[0]!.pubkey.toBase58(), ata.toBase58()) + assert.deepEqual( + instruction.keys + .slice(2) + .map(({ pubkey, isSigner }) => ({ pubkey: pubkey.toBase58(), isSigner })), + [ + { pubkey: MULTISIG, isSigner: false }, + { pubkey: MULTISIG_SIGNER, isSigner: true }, + ], + ) + }) + + it('defaults authority to payer and supports zero through maximum u64 allowances', async () => { + const zero = await generate({ amount: 0n }) + const maximum = await generate({ authority: undefined, amount: U64_MAX }) + + assert.equal(zero.instructions[0]!.data.readBigUInt64LE(1), 0n) + assert.equal(maximum.instructions[0]!.keys[2]!.pubkey.toBase58(), PAYER) + assert.equal(maximum.instructions[0]!.data.readBigUInt64LE(1), U64_MAX) + }) + }) + + describe('validation', () => { + it('rejects invalid parameters', async () => { + for (const [opts, param] of [ + [{ tokenAddress: 'invalid' }, 'tokenAddress'], + [{ tokenAccount: 'invalid' }, 'tokenAccount'], + [{ delegate: 'invalid' }, 'delegate'], + [{ authority: 'invalid' }, 'authority'], + [{ amount: 1 }, 'amount'], + [{ amount: U64_MAX + 1n }, 'amount'], + [{ multisigSigners: 'invalid' }, 'multisigSigners'], + [{ multisigSigners: ['invalid'] }, 'multisigSigners[0]'], + ] as const) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + + it('rejects a missing token account before submission', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain(TOKEN_PROGRAM_ID, false)).generateUnsignedApproveToken( + { + payer: PAYER, + tokenAddress: TOKEN.toBase58(), + delegate: DELEGATE, + amount: 1n, + }, + ), + (err: unknown) => err instanceof CCIPTokenAccountNotFoundError, + ) + }) + + it('rejects missing and non-token mints', async () => { + await assert.rejects( + () => generate({}, null), + (err: unknown) => err instanceof CCIPTokenMintNotFoundError, + ) + await assert.rejects( + () => generate({}, Keypair.generate().publicKey), + (err: unknown) => err instanceof CCIPTokenMintInvalidError, + ) + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await SolanaTokenManager.fromChain(submitChain()).approveToken({ + tokenAddress: TOKEN.toBase58(), + delegate: DELEGATE, + amount: 1n, + wallet: WALLET, + }) + assert.deepEqual(result, { hash: HASH }) + }) + + it('requires unsigned generation for SPL multisig and external authorities', async () => { + for (const opts of [ + { authority: MULTISIG, multisigSigners: [MULTISIG_SIGNER] }, + { authority: AUTHORITY }, + ]) { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).approveToken({ + tokenAddress: TOKEN.toBase58(), + delegate: DELEGATE, + amount: 1n, + wallet: WALLET, + ...opts, + }), + (err: unknown) => err instanceof CCTParamsInvalidError, + ) + } + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/approve-token.ts b/ccip-sdk/src/cct/solana/token/operations/approve-token.ts new file mode 100644 index 000000000..c6254ed36 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/approve-token.ts @@ -0,0 +1,151 @@ +import { createApproveInstruction } from '@solana/spl-token' +import type { PublicKey, TransactionInstruction } 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 { + U64_MAX, + parsePublicKey, + resolveExistingTokenAccount, + validateAuthorityMatchesWallet, + validateBigInt, +} from '../../validate.ts' + +type ApproveTokenParams = { + /** SPL token mint address. */ + tokenAddress: string + /** Token account to approve from. Defaults to the authority's existing associated token account. */ + tokenAccount?: string + /** Trusted delegate address authorized to transfer tokens. Re-approval replaces the current delegate. */ + delegate: string + /** + * Allowance in base units (not human-readable tokens). Re-approval replaces the current allowance; + * use `0n` to clear it. E.g., 1_000_000n with 6 decimals = 1 token. Maximum u64: 2^64 - 1. + */ + amount: bigint + /** Token account owner. Defaults to `payer` for single-signer transactions. */ + authority?: string + /** SPL Token multisig member addresses. Required when authority is an SPL Token multisig. */ + multisigSigners?: string[] +} + +type ParsedApproveTokenParams = { + tokenAddress: PublicKey + tokenAccount?: PublicKey + delegate: PublicKey + amount: bigint + authority: PublicKey + multisigSigners: PublicKey[] +} + +/** Parameters for unsigned Solana SPL Token delegate approval. */ +export type GenerateApproveTokenParams = SolanaGenerateParams + +/** Unsigned Solana SPL Token delegate approval result. */ +export type GenerateApproveTokenResult = UnsignedSolanaTx + +/** Parameters for executing Solana SPL Token delegate approval. */ +export type ExecuteApproveTokenParams = SolanaExecuteParams + +/** Result of executing Solana SPL Token delegate approval. */ +export type ExecuteApproveTokenResult = TransactionResult + +/** Approves a delegate to transfer up to an allowance from an SPL token account. */ +export class ApproveToken extends SolanaOperation< + ApproveTokenParams, + UnsignedSolanaTx, + ParsedApproveTokenParams +> { + readonly name = 'approveToken' + + /** Parses public keys, allowance, and optional SPL Token multisig signers. */ + protected override parse(params: GenerateApproveTokenParams): ParsedApproveTokenParams { + validateBigInt(this.name, 'amount', params.amount, 0n, U64_MAX) + if (params.multisigSigners !== undefined && !Array.isArray(params.multisigSigners)) { + throw new CCTParamsInvalidError(this.name, 'multisigSigners', 'must be an array') + } + + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + tokenAccount: + params.tokenAccount === undefined + ? undefined + : parsePublicKey(this.name, 'tokenAccount', params.tokenAccount), + delegate: parsePublicKey(this.name, 'delegate', params.delegate), + amount: params.amount, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + multisigSigners: (params.multisigSigners ?? []).map((signer, i) => + parsePublicKey(this.name, `multisigSigners[${i}]`, signer), + ), + } + } + + /** Builds an SPL Token `Approve` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedApproveTokenParams, + ): Promise { + const { tokenAccount, tokenProgram } = await resolveExistingTokenAccount( + chain.connection, + opts.tokenAddress, + opts.authority, + opts.tokenAccount, + ) + + const instructions: TransactionInstruction[] = [ + createApproveInstruction( + tokenAccount, + opts.delegate, + opts.authority, + opts.amount, + opts.multisigSigners, + tokenProgram, + ), + ] + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, tokenAccount = ${tokenAccount.toBase58()}, delegate = ${opts.delegate.toBase58()}, amount = ${opts.amount}`, + ) + return { family: ChainFamily.Solana, instructions, mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the token account owner wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteApproveTokenParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (parsed.multisigSigners.length > 0) { + throw new CCTParamsInvalidError( + this.name, + 'multisigSigners', + 'requires externally signed transactions; use generateUnsignedApproveToken', + ) + } + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'approveToken requires authority to be the executing wallet. Use generateUnsignedApproveToken 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/index.ts b/ccip-sdk/src/cct/solana/token/operations/index.ts index 51439b7d1..037158331 100644 --- a/ccip-sdk/src/cct/solana/token/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token/operations/index.ts @@ -1,3 +1,4 @@ +export * from './approve-token.ts' export * from './create-token-account.ts' export * from './deploy-token.ts' export * from './mint-tokens.ts' diff --git a/ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts index d33be6e4b..26470f990 100644 --- a/ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts +++ b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts @@ -1,11 +1,9 @@ -import { TokenAccountNotFoundError, createMintToInstruction, getAccount } from '@solana/spl-token' +import { createMintToInstruction } from '@solana/spl-token' import type { PublicKey, TransactionInstruction } from '@solana/web3.js' -import { CCIPTokenAccountNotFoundError } from '../../../../errors/index.ts' 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 { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' import { @@ -17,6 +15,7 @@ import { submit } from '../../submit.ts' import { U64_MAX, parsePublicKey, + resolveExistingTokenAccount, validateAuthorityMatchesWallet, validateBigInt, } from '../../validate.ts' @@ -96,27 +95,16 @@ export class MintTokens extends SolanaOperation< chain: SolanaChain, opts: ParsedMintTokensParams, ): Promise { - const { ata, tokenProgram } = await resolveATA( + const { tokenAccount, tokenProgram } = await resolveExistingTokenAccount( chain.connection, opts.tokenAddress, opts.recipient, ) - try { - await getAccount(chain.connection, ata, undefined, tokenProgram) - } catch (error) { - if (error instanceof TokenAccountNotFoundError) { - throw new CCIPTokenAccountNotFoundError( - opts.tokenAddress.toBase58(), - opts.recipient.toBase58(), - ) - } - throw error - } const instructions: TransactionInstruction[] = [ createMintToInstruction( opts.tokenAddress, - ata, + tokenAccount, opts.authority, opts.amount, opts.multisigSigners, diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index 49cb7ef2f..3ce682196 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -1,9 +1,11 @@ import { Buffer } from 'buffer' -import { PublicKey } from '@solana/web3.js' +import { TokenAccountNotFoundError, getAccount } from '@solana/spl-token' +import { type Connection, PublicKey } from '@solana/web3.js' -import { CCIPAddressInvalidError } from '../../errors/index.ts' +import { CCIPAddressInvalidError, CCIPTokenAccountNotFoundError } from '../../errors/index.ts' import { ChainFamily } from '../../networks.ts' +import { resolveATA } from '../../solana/utils.ts' import { CCTParamsInvalidError } from '../errors.ts' import { type PoolProgramRef, @@ -262,3 +264,28 @@ export function parseNonEmptyHexBytes( if (!bytes.length) throw new CCTParamsInvalidError(operation, param, 'must not be empty') return bytes } + +/** + * Resolves an existing token account, defaulting to the holder's associated token account. + * @throws {@link CCIPTokenAccountNotFoundError} If the token account does not exist. + */ +export async function resolveExistingTokenAccount( + connection: Connection, + tokenAddress: PublicKey, + holder: PublicKey, + tokenAccount?: PublicKey, +): Promise<{ tokenAccount: PublicKey; tokenProgram: PublicKey }> { + const { ata, tokenProgram } = await resolveATA(connection, tokenAddress, holder) + const account = tokenAccount ?? ata + + try { + await getAccount(connection, account, undefined, tokenProgram) + } catch (error) { + if (error instanceof TokenAccountNotFoundError) { + throw new CCIPTokenAccountNotFoundError(tokenAddress.toBase58(), holder.toBase58()) + } + throw error + } + + return { tokenAccount: account, tokenProgram } +} From d1e7af7c521df2bb7233fcfa039fb278128fab09 Mon Sep 17 00:00:00 2001 From: Marek Sadura Date: Fri, 28 Aug 2026 14:58:41 +0200 Subject: [PATCH 72/87] Export all contracts.ts to expose abis --- ccip-sdk/src/cct/evm/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index be74f3d8a..49a50f139 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -642,7 +642,9 @@ export type { GetSupportedTokensParams, GetSupportedTokensResult, } from './token-admin-registry/operations/get-supported-tokens.ts' +export * from './token-admin-registry/contracts.ts' export type { DeployTokenParams } from './token/operations/deploy-token.ts' +export * from './token/contracts.ts' export type { DeployTokenPoolParams, DeployableTokenPoolType, @@ -661,8 +663,10 @@ export type { } from './token-pool/operations/get-token-pool-remotes.ts' /** The lane types `GetTokenPoolRemotesResult` is keyed over; shared with `Chain.getTokenPoolRemotes`. */ export type { RateLimiterState, TokenPoolRemote } from '../../chain.ts' +export * from './token-pool/contracts.ts' export type { DeployLockboxParams } from './lockbox/operations/deploy-lockbox.ts' export type { AuthorizeLockboxCallersParams } from './lockbox/operations/authorize-callers.ts' +export * from './lockbox/contracts.ts' export type { DeployArtifact, DeployResult, From 2e4c6c786d0b42774f2a53957a91646891f6f975 Mon Sep 17 00:00:00 2001 From: Mervin Date: Wed, 2 Sep 2026 17:08:40 +0800 Subject: [PATCH 73/87] feat(cct-sdk): Add provider liquidity op solana (#380) * feat: add transfer pool ownership op solana * feat: add accept pool ownership op solana * feat: add transfer authority op solana * fix: update export barrel * feat: add mint tokens op solana * fix: address comments * fix: address comments * feat: add set can accept liquidity op solana * fix: add lock release token pool idl * feat: add set rebalancer op solana * fix: update tsdoc * feat: add approve token op solana * feat: add provider liquidity op solana * fix: address comments * fix: revert unrelated changes * fix: add preflight checks * fix: update tsdoc * fix: extract validate pool liquidity config * fix: lint errors * fix: address comments * feat(cct-sdk): Add withdraw liquidity op solana (#384) * feat: add withdraw liquidity op solana * fix: add preflight checks * fix: lint errors --- ccip-sdk/src/cct/solana/index.test.ts | 4 + ccip-sdk/src/cct/solana/index.ts | 173 ++++++++++ .../cct/solana/token-pool/operations/index.ts | 2 + .../operations/provide-liquidity.test.ts | 302 ++++++++++++++++++ .../operations/provide-liquidity.ts | 173 ++++++++++ .../operations/withdraw-liquidity.test.ts | 288 +++++++++++++++++ .../operations/withdraw-liquidity.ts | 161 ++++++++++ ccip-sdk/src/cct/solana/validate.ts | 86 ++++- 8 files changed, 1183 insertions(+), 6 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/withdraw-liquidity.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-pool/operations/withdraw-liquidity.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index abb0890ca..7c2195428 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -77,6 +77,10 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(typeof cct.setChainRateLimit, 'function') assert.equal(typeof cct.generateUnsignedSetRateLimitAdmin, 'function') assert.equal(typeof cct.setRateLimitAdmin, 'function') + assert.equal(typeof cct.generateUnsignedProvideLiquidity, 'function') + assert.equal(typeof cct.provideLiquidity, 'function') + assert.equal(typeof cct.generateUnsignedWithdrawLiquidity, 'function') + assert.equal(typeof cct.withdrawLiquidity, 'function') assert.equal(typeof cct.generateUnsignedSetRebalancer, 'function') assert.equal(typeof cct.setRebalancer, 'function') assert.equal(typeof cct.generateUnsignedTransferOwnership, 'function') diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index debff5654..0f093a16d 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -71,6 +71,8 @@ import { type ExecuteEditChainRemoteConfigResult, type ExecuteInitChainRemoteConfigParams, type ExecuteInitChainRemoteConfigResult, + type ExecuteProvideLiquidityParams, + type ExecuteProvideLiquidityResult, type ExecuteRemoveFromAllowlistParams, type ExecuteRemoveFromAllowlistResult, type ExecuteSetCanAcceptLiquidityParams, @@ -83,6 +85,8 @@ import { type ExecuteSetRebalancerResult, type ExecuteTransferOwnershipParams, type ExecuteTransferOwnershipResult, + type ExecuteWithdrawLiquidityParams, + type ExecuteWithdrawLiquidityResult, type GenerateAcceptOwnershipParams, type GenerateAcceptOwnershipResult, type GenerateAppendRemotePoolAddressesParams, @@ -101,6 +105,8 @@ import { type GenerateEditChainRemoteConfigResult, type GenerateInitChainRemoteConfigParams, type GenerateInitChainRemoteConfigResult, + type GenerateProvideLiquidityParams, + type GenerateProvideLiquidityResult, type GenerateRemoveFromAllowlistParams, type GenerateRemoveFromAllowlistResult, type GenerateSetCanAcceptLiquidityParams, @@ -113,6 +119,8 @@ import { type GenerateSetRebalancerResult, type GenerateTransferOwnershipParams, type GenerateTransferOwnershipResult, + type GenerateWithdrawLiquidityParams, + type GenerateWithdrawLiquidityResult, type GetTokenPoolRemotesParams, type GetTokenPoolRemotesResult, type GetTokenPoolStateParams, @@ -130,12 +138,14 @@ import { GetTokenPoolRemotes, GetTokenPoolState, InitChainRemoteConfig, + ProvideLiquidity, RemoveFromAllowlist, SetCanAcceptLiquidity, SetChainRateLimit, SetRateLimitAdmin, SetRebalancer, TransferOwnership, + WithdrawLiquidity, } from './token-pool/operations/index.ts' import { type ExecuteApproveTokenParams, @@ -195,12 +205,14 @@ export class SolanaTokenManager extends TokenManager readonly #getTokenPoolRemotes = new GetTokenPoolRemotes() readonly #getTokenPoolState = new GetTokenPoolState() readonly #initChainRemoteConfig = new InitChainRemoteConfig() + readonly #provideLiquidity = new ProvideLiquidity() readonly #removeFromAllowlist = new RemoveFromAllowlist() readonly #setCanAcceptLiquidity = new SetCanAcceptLiquidity() readonly #setChainRateLimit = new SetChainRateLimit() readonly #setRateLimitAdmin = new SetRateLimitAdmin() readonly #setRebalancer = new SetRebalancer() readonly #transferOwnership = new TransferOwnership() + readonly #withdrawLiquidity = new WithdrawLiquidity() /** Creates a Solana CCT manager for an existing chain. */ constructor(chain: SolanaChain) { @@ -1142,6 +1154,167 @@ export class SolanaTokenManager extends TokenManager return this.#setRateLimitAdmin.execute(this.chain, opts) } + /** + * Builds an unsigned instruction to deposit a rebalancer's tokens into a lock-release pool. + * Pass `poolType: 'lock-release'` or a compatible `poolProgramAddress`; a custom program must + * have the canonical lock-release `provideLiquidity` instruction and account layout. `authority` + * defaults to `payer`. `amount` is a positive u64 in base units. + * + * @remarks The pool config must have `canAcceptLiquidity: true` and a `rebalancer` equal to the + * transaction authority. The authority's ATA for `tokenAddress` must exist, hold at least `amount`, + * and delegate at least `amount` to the pool signer PDA; use {@link generateUnsignedApproveToken}. + * + * @see {@link provideLiquidity} + * @see {@link generateUnsignedApproveToken} + * @see {@link setRebalancer} + * @see {@link setCanAcceptLiquidity} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter, address, or amount 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 CCIPTokenPoolStateNotFoundError} If the token pool state is missing. + * @throws {@link CCIPTokenAccountNotFoundError} If the rebalancer or pool vault ATA is missing; create it first. + * + * @example Prepare and generate liquidity instructions + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const amount = 1_000_000n + * const { config } = await cct.getTokenPoolState({ + * tokenAddress: mint, + * poolType: 'lock-release', + * }) + * const approval = await cct.generateUnsignedApproveToken({ + * payer: rebalancer, + * tokenAddress: mint, + * delegate: config.poolSigner, + * amount, + * }) + * const liquidity = await cct.generateUnsignedProvideLiquidity({ + * payer: rebalancer, + * tokenAddress: mint, + * poolType: 'lock-release', + * amount, + * }) + * ``` + */ + generateUnsignedProvideLiquidity( + opts: GenerateProvideLiquidityParams, + ): Promise { + return this.#provideLiquidity.generate(this.chain, opts) + } + + /** + * Deposits tokens from the executing rebalancer wallet into a lock-release pool. + * Pass `poolType: 'lock-release'` or a compatible `poolProgramAddress`; a custom program must + * have the canonical lock-release `provideLiquidity` instruction and account layout. The wallet's + * associated token account must exist and hold the positive u64 `amount` in base units. + * + * @remarks The pool config must have `canAcceptLiquidity: true` and a `rebalancer` equal to the + * transaction authority. Before this operation, the rebalancer ATA must delegate at least `amount` + * to the pool signer PDA; use {@link approveToken} first. + * + * @see {@link generateUnsignedProvideLiquidity} + * @see {@link approveToken} + * @see {@link setRebalancer} + * @see {@link setCanAcceptLiquidity} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter, address, or amount is invalid, or + * the authority differs from the executing wallet. + * @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 CCIPTokenPoolStateNotFoundError} If the token pool state is missing. + * @throws {@link CCIPTokenAccountNotFoundError} If the rebalancer or pool vault ATA is missing; create it first. + * @throws {@link CCTTxFailedError} If the source ATA does not delegate enough tokens to the pool + * signer, the pool rejects the rebalancer, liquidity is disabled, the token account lacks funds, + * or simulation/submission fails. + * + * @example Prepare and provide liquidity + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const amount = 1_000_000n + * const { config } = await cct.getTokenPoolState({ + * tokenAddress: mint, + * poolType: 'lock-release', + * }) + * await cct.approveToken({ wallet, tokenAddress: mint, delegate: config.poolSigner, amount }) + * await cct.provideLiquidity({ wallet, tokenAddress: mint, poolType: 'lock-release', amount }) + * ``` + */ + provideLiquidity(opts: ExecuteProvideLiquidityParams): Promise { + return this.#provideLiquidity.execute(this.chain, opts) + } + + /** + * Builds an unsigned instruction to withdraw tokens from a lock-release pool to a rebalancer's + * associated token account. Pass `poolType: 'lock-release'` or a compatible `poolProgramAddress`; + * a custom program must have the canonical lock-release `withdrawLiquidity` instruction and account + * layout. `authority` defaults to `payer`. `amount` is a positive u64 in base units. + * + * @remarks The pool config must have `canAcceptLiquidity: true` and a `rebalancer` equal to the + * transaction authority. The rebalancer's associated token account must already exist. + * + * @see {@link withdrawLiquidity} + * @see {@link generateUnsignedSetRebalancer} + * @see {@link generateUnsignedSetCanAcceptLiquidity} + * + * @throws {@link CCTParamsInvalidError} If a pool parameter, address, or amount 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 Generate a liquidity withdrawal instruction + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const withdrawal = await cct.generateUnsignedWithdrawLiquidity({ + * payer: rebalancer, + * tokenAddress: mint, + * poolType: 'lock-release', + * amount: 1_000_000n, + * }) + * ``` + */ + generateUnsignedWithdrawLiquidity( + opts: GenerateWithdrawLiquidityParams, + ): Promise { + return this.#withdrawLiquidity.generate(this.chain, opts) + } + + /** + * Withdraws tokens from a lock-release pool into the executing rebalancer wallet's associated + * token account. Pass `poolType: 'lock-release'` or a compatible `poolProgramAddress`; a custom + * program must have the canonical lock-release `withdrawLiquidity` instruction and account layout. + * The wallet's associated token account must exist. `amount` is a positive u64 in base units. + * + * @remarks The pool config must have `canAcceptLiquidity: true` and a `rebalancer` equal to the + * transaction authority. + * + * @see {@link generateUnsignedWithdrawLiquidity} + * @see {@link setRebalancer} + * @see {@link setCanAcceptLiquidity} + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If a pool parameter, address, or amount is invalid, or + * the authority differs from the executing wallet. + * @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 the pool rejects the rebalancer, liquidity is disabled, + * lacks liquidity, the token account does not exist, or simulation/submission fails. + * + * @example Withdraw liquidity + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.withdrawLiquidity({ + * wallet, + * tokenAddress: mint, + * poolType: 'lock-release', + * amount: 1_000_000n, + * }) + * ``` + */ + withdrawLiquidity(opts: ExecuteWithdrawLiquidityParams): Promise { + return this.#withdrawLiquidity.execute(this.chain, opts) + } + /** * Builds an unsigned instruction that sets whether an initialized Solana lock-release token pool * accepts `provideLiquidity` deposits and `withdrawLiquidity` transfers. Pass canonical diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts index 7ac19b448..fa19e29e0 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/index.ts @@ -9,9 +9,11 @@ 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 './provide-liquidity.ts' export * from './remove-from-allowlist.ts' export * from './set-can-accept-liquidity.ts' export * from './set-chain-rate-limit.ts' export * from './set-rate-limit-admin.ts' export * from './set-rebalancer.ts' export * from './transfer-ownership.ts' +export * from './withdraw-liquidity.ts' diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.test.ts new file mode 100644 index 000000000..5987ddae8 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.test.ts @@ -0,0 +1,302 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { BorshAccountsCoder } from '@coral-xyz/anchor' +import { AccountLayout, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { lockReleaseTokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { + deriveTokenPoolConfigPda, + deriveTokenPoolSignerPda, + 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 tokenAccount(delegate?: PublicKey, delegatedAmount = 0n, amount = 1_000_000n) { + const data = Buffer.alloc(AccountLayout.span) + AccountLayout.encode( + { + mint: new PublicKey(TOKEN), + owner: new PublicKey(AUTHORITY), + amount, + delegateOption: delegate ? 1 : 0, + delegate: delegate ?? PublicKey.default, + state: 1, + isNativeOption: 0, + isNative: 0n, + delegatedAmount, + closeAuthorityOption: 0, + closeAuthority: PublicKey.default, + }, + data, + ) + return { owner: TOKEN_PROGRAM_ID, data } +} + +function poolState(poolProgram: PublicKey, rebalancer = new PublicKey(AUTHORITY), accepts = true) { + const mint = new PublicKey(TOKEN) + const poolSigner = deriveTokenPoolSignerPda(poolProgram, mint) + return Buffer.concat([ + BorshAccountsCoder.accountDiscriminator('State'), + Buffer.from([1]), + TOKEN_PROGRAM_ID.toBuffer(), + mint.toBuffer(), + Buffer.from([9]), + poolSigner.toBuffer(), + PublicKey.default.toBuffer(), + new PublicKey(AUTHORITY).toBuffer(), + PublicKey.default.toBuffer(), + PublicKey.default.toBuffer(), + PublicKey.default.toBuffer(), + PublicKey.default.toBuffer(), + rebalancer.toBuffer(), + Buffer.from([accepts ? 1 : 0, 0]), + Buffer.alloc(4), + PublicKey.default.toBuffer(), + ]) +} + +function chain( + poolProgram = resolveTokenPoolProgram('lock-release'), + rebalancer = new PublicKey(AUTHORITY), + acceptsLiquidity = true, + sourceBalance = 1_000_000n, +): SolanaChain { + const poolSigner = deriveTokenPoolSignerPda(poolProgram, new PublicKey(TOKEN)) + const state = deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)) + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async (address: PublicKey) => + address.equals(state) + ? { owner: poolProgram, data: poolState(poolProgram, rebalancer, acceptsLiquidity) } + : tokenAccount(poolSigner, 1_000_000n, sourceBalance), + }, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + const poolSigner = deriveTokenPoolSignerPda( + resolveTokenPoolProgram('lock-release'), + new PublicKey(TOKEN), + ) + const poolProgram = resolveTokenPoolProgram('lock-release') + const state = deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)) + return Object.assign(chain(), { + connection: { + getAccountInfo: async (address: PublicKey) => + address.equals(state) + ? { owner: poolProgram, data: poolState(poolProgram, WALLET.publicKey) } + : tokenAccount(poolSigner, 1_000_000n), + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + }) +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(chain()).generateUnsignedProvideLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + payer: PAYER, + authority: AUTHORITY, + amount: 1_000_000n, + ...opts, + }) +} + +describe('ProvideLiquidity (cct/solana)', () => { + describe('generate', () => { + it('builds the lock-release pool liquidity instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('lock-release') + const token = new PublicKey(TOKEN) + const poolSigner = deriveTokenPoolSignerPda(poolProgram, token) + + 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, token).toBase58(), + isSigner: false, + isWritable: false, + }, + { pubkey: TOKEN_PROGRAM_ID.toBase58(), isSigner: false, isWritable: false }, + { pubkey: TOKEN, isSigner: false, isWritable: true }, + { pubkey: poolSigner.toBase58(), isSigner: false, isWritable: false }, + { + pubkey: getAssociatedTokenAddressSync(token, poolSigner, true).toBase58(), + isSigner: false, + isWritable: true, + }, + { + pubkey: getAssociatedTokenAddressSync(token, new PublicKey(AUTHORITY), true).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: AUTHORITY, isSigner: true, isWritable: false }, + ], + ) + const decoded = lockReleaseTokenPoolCoder.instruction.decode(instruction!.data) + assert.ok(decoded) + assert.equal(decoded.name, 'provideLiquidity') + assert.equal( + (decoded.data as { amount: { toString(): string } }).amount.toString(), + '1000000', + ) + }) + + it('explains failed liquidity preflight checks', async () => { + for (const [pool, hint] of [ + [chain(resolveTokenPoolProgram('lock-release'), PublicKey.default), 'setRebalancer'], + [ + chain(resolveTokenPoolProgram('lock-release'), new PublicKey(AUTHORITY), false), + 'setCanAcceptLiquidity(true)', + ], + [ + chain(resolveTokenPoolProgram('lock-release'), new PublicKey(AUTHORITY), true, 0n), + 'mint or transfer tokens first', + ], + ] as const) { + await assert.rejects( + () => + SolanaTokenManager.fromChain(pool).generateUnsignedProvideLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + payer: PAYER, + authority: AUTHORITY, + amount: 1n, + }), + (error: unknown) => error instanceof CCTTxFailedError && error.message.includes(hint), + ) + } + }) + + it('defaults authority to payer', async () => { + const unsigned = await SolanaTokenManager.fromChain( + chain(resolveTokenPoolProgram('lock-release'), new PublicKey(PAYER)), + ).generateUnsignedProvideLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + payer: PAYER, + amount: 1_000_000n, + }) + + assert.equal(unsigned.instructions[0]!.keys[6]!.pubkey.toBase58(), PAYER) + }) + + it('uses a source account that can be delegated to the pool signer', async () => { + const poolProgram = resolveTokenPoolProgram('lock-release') + const poolSigner = deriveTokenPoolSignerPda(poolProgram, new PublicKey(TOKEN)) + const approval = await SolanaTokenManager.fromChain(chain()).generateUnsignedApproveToken({ + payer: AUTHORITY, + tokenAddress: TOKEN, + delegate: poolSigner.toBase58(), + amount: 1_000_000n, + }) + const liquidity = await generate() + + assert.equal(approval.instructions[0]!.keys[1]!.pubkey.toBase58(), poolSigner.toBase58()) + assert.equal( + approval.instructions[0]!.keys[0]!.pubkey.toBase58(), + liquidity.instructions[0]!.keys[5]!.pubkey.toBase58(), + ) + assert.equal(approval.instructions[0]!.data.readBigUInt64LE(1), 1_000_000n) + }) + + it('supports a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await SolanaTokenManager.fromChain( + chain(new PublicKey(poolProgramAddress)), + ).generateUnsignedProvideLiquidity({ + tokenAddress: TOKEN, + poolProgramAddress, + payer: PAYER, + authority: AUTHORITY, + amount: 1_000_000n, + }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects invalid public keys, amounts, and burn-mint pools', async () => { + for (const [opts, param] of [ + [{ tokenAddress: 'invalid' }, 'tokenAddress'], + [{ authority: 'invalid' }, 'authority'], + [{ amount: 0n }, 'amount'], + [{ amount: 0x1_0000_0000_0000_0000n }, 'amount'], + [{ poolType: 'burn-mint' as const }, 'poolType'], + [ + { + poolType: undefined, + poolProgramAddress: resolveTokenPoolProgram('burn-mint').toBase58(), + }, + 'poolProgramAddress', + ], + ]) { + 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()).provideLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + amount: 1_000_000n, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed liquidity provision', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).provideLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + amount: 1_000_000n, + authority: AUTHORITY, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'provideLiquidity' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts new file mode 100644 index 000000000..fefb7708b --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts @@ -0,0 +1,173 @@ +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 { CCTTxFailedError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type CustomPoolProgramRef, + type LockReleasePoolProgramRef, + createLockReleaseTokenPoolProgram, + deriveTokenPoolConfigPda, + deriveTokenPoolSignerPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + U64_MAX, + parsePublicKey, + resolveExistingTokenAccount, + resolveLockReleasePoolProgram, + validateAuthorityMatchesWallet, + validateBigInt, + validateDelegation, + validatePoolLiquidityConfig, +} from '../../validate.ts' + +type PoolProgramRef = LockReleasePoolProgramRef | CustomPoolProgramRef + +type ProvideLiquidityParams = PoolProgramRef & { + /** Token mint address managed by the lock-release pool. */ + tokenAddress: string + /** Amount to deposit in base units. Must be a positive u64. */ + amount: bigint + /** Pool rebalancer whose ATA for `tokenAddress` must hold `amount` and delegate it to the pool signer. Defaults to `payer`. */ + authority?: string +} + +type ParsedProvideLiquidityParams = { + tokenAddress: PublicKey + amount: bigint + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey +} + +/** Parameters for unsigned Solana lock-release pool liquidity provision. */ +export type GenerateProvideLiquidityParams = SolanaGenerateParams + +/** Unsigned Solana lock-release pool liquidity provision result. */ +export type GenerateProvideLiquidityResult = UnsignedSolanaTx + +/** Parameters for providing Solana lock-release pool liquidity. */ +export type ExecuteProvideLiquidityParams = SolanaExecuteParams + +/** Result of providing Solana lock-release pool liquidity. */ +export type ExecuteProvideLiquidityResult = TransactionResult + +/** Deposits tokens from a rebalancer's associated token account into a lock-release pool. */ +export class ProvideLiquidity extends SolanaOperation< + ProvideLiquidityParams, + UnsignedSolanaTx, + ParsedProvideLiquidityParams +> { + readonly name = 'provideLiquidity' + + /** Parses public keys, validates amount, and defaults authority to payer without mutating caller params. */ + protected override parse(params: GenerateProvideLiquidityParams): ParsedProvideLiquidityParams { + validateBigInt(this.name, 'amount', params.amount, 1n, U64_MAX) + + const poolProgram = resolveLockReleasePoolProgram(this.name, params) + const payer = parsePublicKey(this.name, 'payer', params.payer) + + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + amount: params.amount, + poolProgram, + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + } + } + + /** Builds the unsigned Solana `provideLiquidity` instruction for a lock-release pool. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedProvideLiquidityParams, + ): Promise { + // The caller must be the configured rebalancer and the pool must accept deposits. + await validatePoolLiquidityConfig( + this.name, + chain, + opts.poolProgram, + opts.tokenAddress, + opts.authority, + ) + + // The rebalancer's source ATA must exist. + const { + tokenAccount: remoteTokenAccount, + tokenProgram, + account: remoteTokenAccountInfo, + } = await resolveExistingTokenAccount(chain.connection, opts.tokenAddress, opts.authority) + const poolSigner = deriveTokenPoolSignerPda(opts.poolProgram, opts.tokenAddress) + + // Avoid an opaque SPL Token insufficient-funds failure. + if (remoteTokenAccountInfo.amount < opts.amount) + throw new CCTTxFailedError( + this.name, + `source token account ${remoteTokenAccount.toBase58()} has ${remoteTokenAccountInfo.amount}, but ${opts.amount} is required; mint or transfer tokens first`, + ) + + // The pool signer transfers from the rebalancer ATA as its SPL Token delegate. + validateDelegation( + this.name, + remoteTokenAccount, + remoteTokenAccountInfo, + poolSigner, + opts.amount, + ) + + // The pool vault ATA must have been created during pool initialization. + const { tokenAccount: poolTokenAccount } = await resolveExistingTokenAccount( + chain.connection, + opts.tokenAddress, + poolSigner, + ) + + const instruction = await createLockReleaseTokenPoolProgram(chain, opts.poolProgram, opts.payer) + .methods.provideLiquidity(new BN(opts.amount.toString())) + .accountsStrict({ + state: deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress), + tokenProgram, + mint: opts.tokenAddress, + poolSigner, + poolTokenAccount, + remoteTokenAccount, + authority: opts.authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}, amount = ${opts.amount}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the rebalancer wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteProvideLiquidityParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'provideLiquidity requires authority to be the executing wallet. Use generateUnsignedProvideLiquidity 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/withdraw-liquidity.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/withdraw-liquidity.test.ts new file mode 100644 index 000000000..d2c301488 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/withdraw-liquidity.test.ts @@ -0,0 +1,288 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { BorshAccountsCoder } from '@coral-xyz/anchor' +import { AccountLayout, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { ChainFamily } from '../../../../networks.ts' +import { lockReleaseTokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import { SolanaTokenManager } from '../../index.ts' +import { + deriveTokenPoolConfigPda, + deriveTokenPoolSignerPda, + 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 tokenAccount(owner: PublicKey, amount = 1_000_000n) { + const data = Buffer.alloc(AccountLayout.span) + AccountLayout.encode( + { + mint: new PublicKey(TOKEN), + owner, + amount, + delegateOption: 0, + delegate: PublicKey.default, + state: 1, + isNativeOption: 0, + isNative: 0n, + delegatedAmount: 0n, + closeAuthorityOption: 0, + closeAuthority: PublicKey.default, + }, + data, + ) + return { owner: TOKEN_PROGRAM_ID, data } +} + +function poolState(poolProgram: PublicKey, rebalancer = new PublicKey(AUTHORITY), accepts = true) { + const mint = new PublicKey(TOKEN) + const poolSigner = deriveTokenPoolSignerPda(poolProgram, mint) + return Buffer.concat([ + BorshAccountsCoder.accountDiscriminator('State'), + Buffer.from([1]), + TOKEN_PROGRAM_ID.toBuffer(), + mint.toBuffer(), + Buffer.from([9]), + poolSigner.toBuffer(), + PublicKey.default.toBuffer(), + new PublicKey(AUTHORITY).toBuffer(), + PublicKey.default.toBuffer(), + PublicKey.default.toBuffer(), + PublicKey.default.toBuffer(), + PublicKey.default.toBuffer(), + rebalancer.toBuffer(), + Buffer.from([accepts ? 1 : 0, 0]), + Buffer.alloc(4), + PublicKey.default.toBuffer(), + ]) +} + +function chain( + poolProgram = resolveTokenPoolProgram('lock-release'), + rebalancer = new PublicKey(AUTHORITY), + acceptsLiquidity = true, + poolBalance = 1_000_000n, +): SolanaChain { + const mint = new PublicKey(TOKEN) + const state = deriveTokenPoolConfigPda(poolProgram, mint) + const poolSigner = deriveTokenPoolSignerPda(poolProgram, mint) + const poolTokenAccount = getAssociatedTokenAddressSync(mint, poolSigner, true) + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async (address: PublicKey) => + address.equals(state) + ? { owner: poolProgram, data: poolState(poolProgram, rebalancer, acceptsLiquidity) } + : address.equals(poolTokenAccount) + ? tokenAccount(poolSigner, poolBalance) + : tokenAccount(rebalancer), + }, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + const poolProgram = resolveTokenPoolProgram('lock-release') + const mint = new PublicKey(TOKEN) + const state = deriveTokenPoolConfigPda(poolProgram, mint) + const poolSigner = deriveTokenPoolSignerPda(poolProgram, mint) + const poolTokenAccount = getAssociatedTokenAddressSync(mint, poolSigner, true) + return Object.assign(chain(), { + connection: { + getAccountInfo: async (address: PublicKey) => + address.equals(state) + ? { owner: poolProgram, data: poolState(poolProgram, WALLET.publicKey) } + : address.equals(poolTokenAccount) + ? tokenAccount(poolSigner) + : tokenAccount(WALLET.publicKey), + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + }) +} + +function generate(opts = {}) { + return SolanaTokenManager.fromChain(chain()).generateUnsignedWithdrawLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + payer: PAYER, + authority: AUTHORITY, + amount: 1_000_000n, + ...opts, + }) +} + +describe('WithdrawLiquidity (cct/solana)', () => { + describe('generate', () => { + it('builds the lock-release pool liquidity instruction', async () => { + const unsigned = await generate() + const [instruction] = unsigned.instructions + const poolProgram = resolveTokenPoolProgram('lock-release') + const token = new PublicKey(TOKEN) + const poolSigner = deriveTokenPoolSignerPda(poolProgram, token) + + 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, token).toBase58(), + isSigner: false, + isWritable: false, + }, + { pubkey: TOKEN_PROGRAM_ID.toBase58(), isSigner: false, isWritable: false }, + { pubkey: TOKEN, isSigner: false, isWritable: true }, + { pubkey: poolSigner.toBase58(), isSigner: false, isWritable: false }, + { + pubkey: getAssociatedTokenAddressSync(token, poolSigner, true).toBase58(), + isSigner: false, + isWritable: true, + }, + { + pubkey: getAssociatedTokenAddressSync(token, new PublicKey(AUTHORITY), true).toBase58(), + isSigner: false, + isWritable: true, + }, + { pubkey: AUTHORITY, isSigner: true, isWritable: false }, + ], + ) + const decoded = lockReleaseTokenPoolCoder.instruction.decode(instruction!.data) + assert.ok(decoded) + assert.equal(decoded.name, 'withdrawLiquidity') + assert.equal( + (decoded.data as { amount: { toString(): string } }).amount.toString(), + '1000000', + ) + }) + + it('explains failed liquidity preflight checks', async () => { + for (const [pool, hint] of [ + [chain(resolveTokenPoolProgram('lock-release'), PublicKey.default), 'setRebalancer'], + [ + chain(resolveTokenPoolProgram('lock-release'), new PublicKey(AUTHORITY), false), + 'setCanAcceptLiquidity(true)', + ], + [ + chain(resolveTokenPoolProgram('lock-release'), new PublicKey(AUTHORITY), true, 0n), + 'pool token account', + ], + ] as const) { + await assert.rejects( + () => + SolanaTokenManager.fromChain(pool).generateUnsignedWithdrawLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + payer: PAYER, + authority: AUTHORITY, + amount: 1n, + }), + (error: unknown) => error instanceof CCTTxFailedError && error.message.includes(hint), + ) + } + }) + + it('defaults authority to payer', async () => { + const unsigned = await SolanaTokenManager.fromChain( + chain(resolveTokenPoolProgram('lock-release'), new PublicKey(PAYER)), + ).generateUnsignedWithdrawLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + payer: PAYER, + amount: 1_000_000n, + }) + + assert.equal(unsigned.instructions[0]!.keys[6]!.pubkey.toBase58(), PAYER) + }) + + it('supports a compatible custom pool program', async () => { + const poolProgramAddress = Keypair.generate().publicKey.toBase58() + const unsigned = await SolanaTokenManager.fromChain( + chain(new PublicKey(poolProgramAddress)), + ).generateUnsignedWithdrawLiquidity({ + tokenAddress: TOKEN, + poolProgramAddress, + payer: PAYER, + authority: AUTHORITY, + amount: 1_000_000n, + }) + + assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) + }) + }) + + describe('validation', () => { + it('rejects invalid public keys, amounts, and burn-mint pools', async () => { + for (const [opts, param] of [ + [{ tokenAddress: 'invalid' }, 'tokenAddress'], + [{ authority: 'invalid' }, 'authority'], + [{ amount: 0n }, 'amount'], + [{ amount: 0x1_0000_0000_0000_0000n }, 'amount'], + [{ poolType: 'burn-mint' as const }, 'poolType'], + [ + { + poolType: undefined, + poolProgramAddress: resolveTokenPoolProgram('burn-mint').toBase58(), + }, + 'poolProgramAddress', + ], + ]) { + 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()).withdrawLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + amount: 1_000_000n, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed liquidity withdrawal', async () => { + await assert.rejects( + () => + SolanaTokenManager.fromChain(chain()).withdrawLiquidity({ + tokenAddress: TOKEN, + poolType: 'lock-release', + amount: 1_000_000n, + authority: AUTHORITY, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'withdrawLiquidity' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/withdraw-liquidity.ts b/ccip-sdk/src/cct/solana/token-pool/operations/withdraw-liquidity.ts new file mode 100644 index 000000000..ed6a16255 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-pool/operations/withdraw-liquidity.ts @@ -0,0 +1,161 @@ +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 { CCTTxFailedError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { + type CustomPoolProgramRef, + type LockReleasePoolProgramRef, + createLockReleaseTokenPoolProgram, + deriveTokenPoolConfigPda, + deriveTokenPoolSignerPda, +} from '../../programs/token-pool.ts' +import { submit } from '../../submit.ts' +import { + U64_MAX, + parsePublicKey, + resolveExistingTokenAccount, + resolveLockReleasePoolProgram, + validateAuthorityMatchesWallet, + validateBigInt, + validatePoolLiquidityConfig, +} from '../../validate.ts' + +type PoolProgramRef = LockReleasePoolProgramRef | CustomPoolProgramRef + +type WithdrawLiquidityParams = PoolProgramRef & { + /** Token mint address managed by the lock-release pool. */ + tokenAddress: string + /** Amount to withdraw in base units. Must be a positive u64. */ + amount: bigint + /** Pool rebalancer that withdraws liquidity. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedWithdrawLiquidityParams = { + tokenAddress: PublicKey + amount: bigint + poolProgram: PublicKey + payer: PublicKey + authority: PublicKey +} + +/** Parameters for unsigned Solana lock-release pool liquidity withdrawal. */ +export type GenerateWithdrawLiquidityParams = SolanaGenerateParams + +/** Unsigned Solana lock-release pool liquidity withdrawal result. */ +export type GenerateWithdrawLiquidityResult = UnsignedSolanaTx + +/** Parameters for withdrawing Solana lock-release pool liquidity. */ +export type ExecuteWithdrawLiquidityParams = SolanaExecuteParams + +/** Result of withdrawing Solana lock-release pool liquidity. */ +export type ExecuteWithdrawLiquidityResult = TransactionResult + +/** Withdraws tokens from a lock-release pool into a rebalancer's associated token account. */ +export class WithdrawLiquidity extends SolanaOperation< + WithdrawLiquidityParams, + UnsignedSolanaTx, + ParsedWithdrawLiquidityParams +> { + readonly name = 'withdrawLiquidity' + + /** Parses public keys, validates amount, and defaults authority to payer without mutating caller params. */ + protected override parse(params: GenerateWithdrawLiquidityParams): ParsedWithdrawLiquidityParams { + validateBigInt(this.name, 'amount', params.amount, 1n, U64_MAX) + + const poolProgram = resolveLockReleasePoolProgram(this.name, params) + const payer = parsePublicKey(this.name, 'payer', params.payer) + + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + amount: params.amount, + poolProgram, + payer, + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + } + } + + /** Builds the unsigned Solana `withdrawLiquidity` instruction for a lock-release pool. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedWithdrawLiquidityParams, + ): Promise { + // The caller must be the configured rebalancer and the pool must accept withdrawals. + await validatePoolLiquidityConfig( + this.name, + chain, + opts.poolProgram, + opts.tokenAddress, + opts.authority, + ) + + // The rebalancer's destination ATA must exist. + const { tokenAccount: remoteTokenAccount, tokenProgram } = await resolveExistingTokenAccount( + chain.connection, + opts.tokenAddress, + opts.authority, + ) + const poolSigner = deriveTokenPoolSignerPda(opts.poolProgram, opts.tokenAddress) + + // The pool vault ATA must have been created during pool initialization and hold the withdrawal. + const { tokenAccount: poolTokenAccount, account: poolTokenAccountInfo } = + await resolveExistingTokenAccount(chain.connection, opts.tokenAddress, poolSigner) + + // Avoid an opaque SPL Token insufficient-funds failure. + if (poolTokenAccountInfo.amount < opts.amount) { + throw new CCTTxFailedError( + this.name, + `pool token account ${poolTokenAccount.toBase58()} has ${poolTokenAccountInfo.amount}, but ${opts.amount} is required`, + ) + } + + const instruction = await createLockReleaseTokenPoolProgram(chain, opts.poolProgram, opts.payer) + .methods.withdrawLiquidity(new BN(opts.amount.toString())) + .accountsStrict({ + state: deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress), + tokenProgram, + mint: opts.tokenAddress, + poolSigner, + poolTokenAccount, + remoteTokenAccount, + authority: opts.authority, + }) + .instruction() + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}, amount = ${opts.amount}`, + ) + return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the rebalancer wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteWithdrawLiquidityParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'withdrawLiquidity requires authority to be the executing wallet. Use generateUnsignedWithdrawLiquidity for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index 3ce682196..17052c88e 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -1,16 +1,23 @@ import { Buffer } from 'buffer' -import { TokenAccountNotFoundError, getAccount } from '@solana/spl-token' +import { type Account, TokenAccountNotFoundError, getAccount } from '@solana/spl-token' import { type Connection, PublicKey } from '@solana/web3.js' -import { CCIPAddressInvalidError, CCIPTokenAccountNotFoundError } from '../../errors/index.ts' +import { + CCIPAddressInvalidError, + CCIPTokenAccountNotFoundError, + CCIPTokenPoolStateNotFoundError, +} from '../../errors/index.ts' import { ChainFamily } from '../../networks.ts' +import type { SolanaChain } from '../../solana/index.ts' import { resolveATA } from '../../solana/utils.ts' -import { CCTParamsInvalidError } from '../errors.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../errors.ts' import { type PoolProgramRef, type TokenPoolType, TOKEN_POOL_PROGRAMS, + decodeTokenPoolState, + deriveTokenPoolConfigPda, resolveTokenPoolProgram, } from './programs/token-pool.ts' @@ -265,6 +272,72 @@ export function parseNonEmptyHexBytes( return bytes } +/** + * Validates that a token account delegates at least an amount to the expected delegate. + * @throws {@link CCTTxFailedError} If the delegate is missing, differs, or has insufficient allowance. + */ +export function validateDelegation( + operation: string, + tokenAccount: PublicKey, + account: Account, + delegate: PublicKey, + amount: bigint, +): void { + if (account.delegate?.equals(delegate) && account.delegatedAmount >= amount) return + + const delegation = !account.delegate + ? 'has no delegate' + : !account.delegate.equals(delegate) + ? `delegates to ${account.delegate.toBase58()}` + : `delegates only ${account.delegatedAmount}` + throw new CCTTxFailedError( + operation, + `token account ${tokenAccount.toBase58()} ${delegation}; delegate at least ${amount} to ${delegate.toBase58()} with approveToken first`, + { + context: { + tokenAccount: tokenAccount.toBase58(), + delegate: account.delegate?.toBase58(), + expectedDelegate: delegate.toBase58(), + delegatedAmount: account.delegatedAmount.toString(), + }, + }, + ) +} + +/** + * Verifies that a rebalancer may move liquidity for a lock-release pool. + * @throws {@link CCIPTokenPoolStateNotFoundError} If the token pool state is missing. + * @throws {@link CCTTxFailedError} If the authority is not the rebalancer or liquidity is disabled. + */ +export async function validatePoolLiquidityConfig( + operation: string, + chain: SolanaChain, + poolProgram: PublicKey, + mint: PublicKey, + authority: PublicKey, +): Promise { + const state = deriveTokenPoolConfigPda(poolProgram, mint) + const account = await chain.connection.getAccountInfo(state) + if (!account) throw new CCIPTokenPoolStateNotFoundError(state.toBase58()) + + const { config } = decodeTokenPoolState(account.data, { + tokenPool: state.toBase58(), + mint: mint.toBase58(), + poolProgram: poolProgram.toBase58(), + accountOwner: account.owner.toBase58(), + }) + if (!config.rebalancer.equals(authority)) + throw new CCTTxFailedError( + operation, + `pool rebalancer is ${config.rebalancer.toBase58()}, not ${authority.toBase58()}; set it with setRebalancer first`, + ) + if (!config.canAcceptLiquidity) + throw new CCTTxFailedError( + operation, + 'pool does not accept liquidity; enable it with setCanAcceptLiquidity(true) first', + ) +} + /** * Resolves an existing token account, defaulting to the holder's associated token account. * @throws {@link CCIPTokenAccountNotFoundError} If the token account does not exist. @@ -274,12 +347,13 @@ export async function resolveExistingTokenAccount( tokenAddress: PublicKey, holder: PublicKey, tokenAccount?: PublicKey, -): Promise<{ tokenAccount: PublicKey; tokenProgram: PublicKey }> { +): Promise<{ tokenAccount: PublicKey; tokenProgram: PublicKey; account: Account }> { const { ata, tokenProgram } = await resolveATA(connection, tokenAddress, holder) const account = tokenAccount ?? ata + let tokenAccountInfo: Account try { - await getAccount(connection, account, undefined, tokenProgram) + tokenAccountInfo = await getAccount(connection, account, undefined, tokenProgram) } catch (error) { if (error instanceof TokenAccountNotFoundError) { throw new CCIPTokenAccountNotFoundError(tokenAddress.toBase58(), holder.toBase58()) @@ -287,5 +361,5 @@ export async function resolveExistingTokenAccount( throw error } - return { tokenAccount: account, tokenProgram } + return { tokenAccount: account, tokenProgram, account: tokenAccountInfo } } From 6deb251e2dc390bbdfbb73e61c12ebacb001692d Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:02:45 +0100 Subject: [PATCH 74/87] feat(cct-sdk): Add apply chain updates evm op (#382) * feat(cct-sdk): Add get token pool remotes evm query * feat(cct-sdk): Add generic EVM param validators * feat(cct-sdk): Read siloed lock/release pool state * feat(cct-sdk): Add pool owner pre-flight and encoder removal ceilings * refactor(cct-sdk): Hoist validate/parse lifecycle to the shared Operation base * feat(cct-sdk): Add apply chain updates evm op * fix(cct-sdk): Reject a declared version that is not the pool's calldata shape * Address PR comments * Fix lint --- ccip-sdk/src/cct/evm/index.ts | 131 ++- .../lockbox/operations/authorize-callers.ts | 2 +- .../evm/lockbox/operations/deploy-lockbox.ts | 2 +- ccip-sdk/src/cct/evm/operation.ts | 34 +- .../operations/accept-admin.ts | 31 +- .../operations/register-admin.test.ts | 4 +- .../operations/register-admin.ts | 6 +- .../operations/set-pool.ts | 2 +- .../operations/transfer-admin.ts | 31 +- .../src/cct/evm/token-pool/contracts.test.ts | 74 +- ccip-sdk/src/cct/evm/token-pool/contracts.ts | 95 +- .../operations/apply-chain-updates.test.ts | 927 ++++++++++++++++++ .../operations/apply-chain-updates.ts | 561 +++++++++++ .../operations/deploy-token-pool.ts | 2 +- .../operations/get-token-pool-state.test.ts | 93 +- .../operations/get-token-pool-state.ts | 46 +- .../operations/transfer-ownership.ts | 2 +- .../cct/evm/token/operations/deploy-token.ts | 2 +- ccip-sdk/src/cct/evm/validate.ts | 172 +++- ccip-sdk/src/cct/operation.ts | 34 +- ccip-sdk/src/cct/solana/operation.ts | 26 +- 21 files changed, 2092 insertions(+), 185 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.ts diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 446007598..665bfc17a 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -43,6 +43,10 @@ import { type TransferAdminParams, TransferAdmin, } from './token-admin-registry/operations/transfer-admin.ts' +import { + type ApplyChainUpdatesParams, + ApplyChainUpdates, +} from './token-pool/operations/apply-chain-updates.ts' import { type DeployTokenPoolParams, DeployTokenPool, @@ -82,6 +86,7 @@ export class EVMTokenManager extends TokenManager { readonly #transferOwnership = new TransferOwnership() readonly #getTokenPoolState = new GetTokenPoolState() readonly #getTokenPoolRemotes = new GetTokenPoolRemotes() + readonly #applyChainUpdates = new ApplyChainUpdates() // Lockbox operations readonly #deployLockbox = new DeployLockbox() @@ -572,8 +577,8 @@ export class EVMTokenManager extends TokenManager { * `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 + * (see the example) — a `SiloedLockReleaseTokenPool` reports no `lockBox`, since it escrows per + * remote chain. 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. @@ -624,6 +629,119 @@ export class EVMTokenManager extends TokenManager { getTokenPoolRemotes(opts: GetTokenPoolRemotesParams): Promise { return this.#getTokenPoolRemotes.query(this.chain, opts) } + + /** + * Applies the pool's remote-lane configuration, signing + submitting with `opts.wallet`. + * @remarks Same version-discriminated params as + * {@link generateUnsignedApplyChainUpdates} — see there for the v1.5.0 vs v1.5.1 divergence. + * `opts.sender` defaults to the wallet's own address (the only address `onlyOwner` can pass) and + * is rejected if it differs, so the wallet must be the pool owner. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid, `version` does not match the + * pool's own generation, or `sender` is given and is not the wallet address / pool owner. As + * with {@link generateUnsignedApplyChainUpdates}, an enabled rate limiter on a **v1.5.0 or + * v1.5.1** pool must satisfy the stricter `0 < rate < capacity`. + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * // `wallet` must sign as the pool owner + * const { hash } = await cct.applyChainUpdates({ + * version: '1.5.1', + * poolAddress: '0xPool...', + * remoteChainSelectorsToRemove: [], + * chainsToAdd: [ + * { + * remoteChainSelector: 16015286601757825753n, + * remoteTokenAddress: '0xRemoteToken...', + * remotePoolAddresses: ['0xRemotePool...'], + * inboundRateLimiterConfig: { enabled: false }, + * outboundRateLimiterConfig: { enabled: false }, + * }, + * ], + * wallet, + * }) + * ``` + */ + applyChainUpdates(opts: EVMExecuteParams): Promise { + return this.#applyChainUpdates.execute(this.chain, opts) + } + + /** + * Builds an unsigned pool `applyChainUpdates` tx (for multisig / offline signing), configuring, + * enabling and disabling the pool's remote lanes: remote token, remote pool(s), and both + * directional rate limits. + * + * @remarks **The parameter shape is version-discriminated**, because the contract's own + * signature changed at v1.5.1 — this is the one CCT pool write where the caller must say which + * generation it is writing for, via `opts.version`: + * + * - `version: '1.5.0'` — a single `chains` array. Each entry carries the enable/disable bit + * inline (`allowed: false` removes the lane) and a **singular** `remotePoolAddress`. + * - `version: '1.5.1'` — removals in `remoteChainSelectorsToRemove`, additions in `chainsToAdd`, + * and each addition carries **plural** `remotePoolAddresses`. This is also the shape for + * v1.6.1 and v2.0.0 pools, whose calldata is byte-identical to v1.5.1's. + * + * The declaration is checked against the pool's on-chain `typeAndVersion`, so writing the wrong + * shape is a parameter error here rather than a tx that reverts on an unknown selector (the two + * signatures have different selectors: `0xdb6327dc` vs `0xe8a1da17`). + * + * Rate limits use the SDK's `enabled` spelling, not the ABI's `isEnabled`, matching the Solana + * counterpart; amounts are in the token's smallest unit. Pass `opts.sender` to pre-flight it + * against the pool's `owner()` — `applyChainUpdates` is `onlyOwner`. + * @throws {@link CCTParamsInvalidError} if any param is invalid, `version` does not match the + * pool's own generation, or `sender` is not the pool owner. An enabled rate limiter must have + * `rate <= capacity` on every version; on a **v1.5.0 or v1.5.1** pool the bound is stricter + * (`0 < rate < capacity`), so a `rate` of `0n` or a `rate` equal to `capacity` is also rejected + * there — v1.6.1 and v2.0.0 allow both. + * + * Each lane array must also be dense (no holes) and free of repeated selectors, and a lane + * being *added* may not use the `0n` selector — the contract would accept it as a permanently + * unroutable lane rather than reverting. `remoteChainSelectorsToRemove` still accepts `0n`, so + * a pool already holding such a lane can be repaired; listing one selector in both + * `chainsToAdd` and `remoteChainSelectorsToRemove` remains the wholesale-replace idiom. + * @throws {@link CCTContractTypeInvalidError} if `poolAddress` is not a supported pool type + * @throws {@link CCTContractVersionUnsupportedError} if the pool reports an unknown version + * @example Enabling a lane on a v1.6.1 pool (the `1.5.1` shape) while retiring an old one: + * ```typescript + * const unsigned = await cct.generateUnsignedApplyChainUpdates({ + * version: '1.5.1', + * poolAddress: '0xPool...', + * sender: '0xPoolOwner...', + * remoteChainSelectorsToRemove: [3478487238524512106n], // arbitrum-sepolia + * chainsToAdd: [ + * { + * remoteChainSelector: 16015286601757825753n, // ethereum-sepolia + * remoteTokenAddress: '0xRemoteToken...', + * remotePoolAddresses: ['0xRemotePool...'], + * inboundRateLimiterConfig: { enabled: true, capacity: 100_000_000n, rate: 167_000n }, + * outboundRateLimiterConfig: { enabled: false }, + * }, + * ], + * }) + * ``` + * @example Disabling a lane on a v1.5.0 pool, where removal is `allowed: false`: + * ```typescript + * const unsigned = await cct.generateUnsignedApplyChainUpdates({ + * version: '1.5.0', + * poolAddress: '0xLegacyPool...', + * chains: [ + * { + * remoteChainSelector: 16015286601757825753n, + * allowed: false, + * remoteTokenAddress: '0xRemoteToken...', + * remotePoolAddress: '0xRemotePool...', // still required, ignored by the contract + * inboundRateLimiterConfig: { enabled: false }, + * outboundRateLimiterConfig: { enabled: false }, + * }, + * ], + * }) + * ``` + */ + generateUnsignedApplyChainUpdates(opts: ApplyChainUpdatesParams): Promise { + return this.#applyChainUpdates.generate(this.chain, opts) + } } export * from '../errors.ts' @@ -661,6 +779,15 @@ export type { GetTokenPoolRemotesParams, GetTokenPoolRemotesResult, } from './token-pool/operations/get-token-pool-remotes.ts' +export type { + ApplyChainUpdatesParamVersion, + ApplyChainUpdatesParams, + ApplyChainUpdatesParamsV1_5_0, + ApplyChainUpdatesParamsV1_5_1, + ChainUpdateV1_5_0, + ChainUpdateV1_5_1, + RateLimitConfigInput, +} from './token-pool/operations/apply-chain-updates.ts' /** The lane types `GetTokenPoolRemotesResult` is keyed over; shared with `Chain.getTokenPoolRemotes`. */ export type { RateLimiterState, TokenPoolRemote } from '../../chain.ts' export * from './token-pool/contracts.ts' diff --git a/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts b/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts index e2650fc6c..003ddb5f5 100644 --- a/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts +++ b/ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts @@ -35,7 +35,7 @@ export class AuthorizeLockboxCallers extends EVMOperation { readonly name = 'deployLockbox' /** Validates the constructor params before building init-code. */ - protected validate(params: DeployLockboxParams): void { + protected override validate(params: DeployLockboxParams): void { validateNonZeroAddress(this.name, 'token', params.token) } diff --git a/ccip-sdk/src/cct/evm/operation.ts b/ccip-sdk/src/cct/evm/operation.ts index 8928da204..eb4b4eb73 100644 --- a/ccip-sdk/src/cct/evm/operation.ts +++ b/ccip-sdk/src/cct/evm/operation.ts @@ -1,10 +1,11 @@ /** - * 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}. + * EVM {@link Operation} lifecycle: prepare (validate → parse) → encode → submit, plus the shared + * wallet-sender pre-flight ({@link EVMOperation.resolveWalletSender}). Deployment ops extend + * {@link EVMDeployOperation}, which also resolves the deployed address. + * + * @remarks The pool-owner pre-flight lives in the token-pool layer as a free helper + * (`assertPoolOwner` in `token-pool/contracts.ts`), so this generic base + * carries no dependency on a specific operation. * * @packageDocumentation */ @@ -79,27 +80,28 @@ export type DeployResult = TransactionResult & { } /** - * 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}. + * EVM CCT write base. Subclasses supply {@link parse} (or {@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< +export abstract class EVMOperation

extends Operation< EVMChain, P, UnsignedEVMTx, - TransactionResult + TransactionResult, + Parsed > { /** Build calldata into an unsigned tx; versioned ops resolve their encoder here. */ protected abstract buildUnsigned( chain: EVMChain, - params: P, + params: Parsed, ): Promise | UnsignedEVMTx - /** Run {@link validate} and {@link buildUnsigned}, applying optional `sender`; no signing. */ + /** Run {@link prepare} and {@link buildUnsigned}, applying optional `sender`; no signing. */ async generate(chain: EVMChain, params: P): Promise { - this.validate(params) + const parsed = this.prepare(params) if (params.sender !== undefined) validateAddress(this.name, 'sender', params.sender) - const unsigned = await this.buildUnsigned(chain, params) + const unsigned = await this.buildUnsigned(chain, parsed) if (params.sender && unsigned.transactions[0]) unsigned.transactions[0].from = params.sender return unsigned } @@ -115,7 +117,7 @@ export abstract class EVMOperation

extends Operat * @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 { + protected async resolveWalletSender(wallet: unknown, sender?: string): Promise { if (!isSigner(wallet)) throw new CCIPWalletInvalidError(wallet) const walletAddress = await wallet.getAddress() if (sender === undefined) return walletAddress 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 index e83ba6cc5..df2899eb3 100644 --- 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 @@ -23,7 +23,7 @@ import { getTokenAdminRegistryInterface, readTokenAdminRegistryConfig } from '.. * @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` + * {@link AcceptAdmin.parse}. {@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}). */ @@ -44,29 +44,34 @@ export type AcceptAdminParams = { sender?: string } +/** {@link AcceptAdminParams} as {@link AcceptAdmin.parse} leaves it: `sender` present and checksummed. */ +type ParsedAcceptAdminParams = AcceptAdminParams & { sender: string } + /** Accepts a pending TokenAdminRegistry administrator role for a token. */ -export class AcceptAdmin extends EVMOperation { +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. + * Validates all addresses before any RPC — `sender` is required here (unlike the base + * `EVMOperation` shape), see the {@link AcceptAdminParams} remarks — and checksums `sender` so + * {@link buildUnsigned} can compare it against the registry's own checksummed + * `pendingAdministrator` without re-asserting it. */ - protected validate(p: AcceptAdminParams): void { + protected override parse(p: AcceptAdminParams): ParsedAcceptAdminParams { validateAddress(this.name, 'tokenAddress', p.tokenAddress) validateAddress(this.name, 'address', p.address) validateAddress(this.name, 'sender', p.sender) + return { ...p, sender: getAddress(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) + protected async buildUnsigned( + chain: EVMChain, + p: ParsedAcceptAdminParams, + ): Promise { const to = await chain.getTokenAdminRegistryFor(p.address) const { administrator, pendingAdministrator } = await readTokenAdminRegistryConfig( chain, @@ -81,7 +86,7 @@ export class AcceptAdmin extends EVMOperation { `no administrator is pending for this token (current administrator: ${administrator}) — nothing to accept`, ) } - if (pendingAdministrator !== sender) { + if (pendingAdministrator !== p.sender) { throw new CCTParamsInvalidError( this.name, 'sender', @@ -99,7 +104,7 @@ export class AcceptAdmin extends EVMOperation { /** * 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 + * broadcast tx. See {@link EVMOperation.resolveWalletSender} 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 @@ -108,7 +113,7 @@ export class AcceptAdmin extends EVMOperation { chain: EVMChain, params: EVMExecuteParams, ): Promise { - const sender = await this.senderBoundToWallet(params.wallet, params.sender) + const sender = await this.resolveWalletSender(params.wallet, params.sender) return super.execute(chain, { ...params, sender }) } } 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 index ed7349e08..497592092 100644 --- 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 @@ -612,7 +612,7 @@ describe('RegisterAdmin (cct/evm token-admin-registry operation)', () => { err instanceof CCTParamsInvalidError && err.context.operation === 'registerAdmin' && err.context.param === 'sender' && - // pins the builder name senderBoundToWallet derives from `this.name`, so the shared + // pins the builder name resolveWalletSender 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'), @@ -620,7 +620,7 @@ describe('RegisterAdmin (cct/evm token-admin-registry operation)', () => { }) it('rejects a malformed sender with CCTParamsInvalidError, not a raw ethers error', async () => { - // senderBoundToWallet validates before getAddress(), which would otherwise throw a raw + // resolveWalletSender 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. 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 index e454efbc6..3b840c720 100644 --- 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 @@ -126,7 +126,7 @@ export class RegisterAdmin extends EVMOperation { readonly name = 'registerAdmin' /** Validates addresses and, if given, `registrationMethod`; no RPC. */ - protected validate(p: RegisterAdminParams): void { + protected override validate(p: RegisterAdminParams): void { validateAddress(this.name, 'tokenAddress', p.tokenAddress) validateAddress(this.name, 'registryModule', p.registryModule) validateAddress(this.name, 'address', p.address) @@ -245,7 +245,7 @@ export class RegisterAdmin extends EVMOperation { /** * 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. + * {@link EVMOperation.resolveWalletSender} 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 */ @@ -253,7 +253,7 @@ export class RegisterAdmin extends EVMOperation { chain: EVMChain, params: EVMExecuteParams, ): Promise { - const sender = await this.senderBoundToWallet(params.wallet, params.sender) + const sender = await this.resolveWalletSender(params.wallet, params.sender) return super.execute(chain, { ...params, sender }) } } 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 index 657607036..9955cbbcf 100644 --- 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 @@ -30,7 +30,7 @@ export class SetPool extends EVMOperation { readonly name = 'setPool' /** Validates all addresses before any RPC. */ - protected validate(p: SetPoolParams): void { + protected override validate(p: SetPoolParams): void { validateAddress(this.name, 'tokenAddress', p.tokenAddress) validateAddress(this.name, 'poolAddress', p.poolAddress) validateAddress(this.name, 'address', p.address) 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 index a09f3b1a6..4a11608af 100644 --- 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 @@ -29,7 +29,7 @@ import { getTokenAdminRegistryInterface, readTokenAdminRegistryConfig } from '.. * @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 + * {@link TransferAdmin.parse}. {@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}). */ @@ -56,27 +56,38 @@ export type TransferAdminParams = { sender?: string } +/** {@link TransferAdminParams} as {@link TransferAdmin.parse} leaves it: `sender` present and checksummed. */ +type ParsedTransferAdminParams = TransferAdminParams & { 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 { +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 { + /** + * Validates all addresses before any RPC, including the presence of `sender` (see above), and + * checksums `sender` so {@link buildUnsigned} can compare it against the registry's own + * checksummed `administrator` without re-asserting it. + */ + protected override parse(p: TransferAdminParams): ParsedTransferAdminParams { validateAddress(this.name, 'tokenAddress', p.tokenAddress) validateAddress(this.name, 'newAdmin', p.newAdmin) validateAddress(this.name, 'address', p.address) validateAddress(this.name, 'sender', p.sender) + return { ...p, sender: getAddress(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 { + protected async buildUnsigned( + chain: EVMChain, + p: ParsedTransferAdminParams, + ): Promise { const to = await chain.getTokenAdminRegistryFor(p.address) const { administrator, pendingAdministrator } = await readTokenAdminRegistryConfig( chain, @@ -84,10 +95,6 @@ export class TransferAdmin extends EVMOperation { 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 @@ -103,7 +110,7 @@ export class TransferAdmin extends EVMOperation { : `token ${p.tokenAddress} is not registered in the TokenAdminRegistry at ${to}; call registerAdmin first`, ) } - if (administrator !== sender) { + if (administrator !== p.sender) { throw new CCTParamsInvalidError( this.name, 'sender', @@ -123,7 +130,7 @@ export class TransferAdmin extends EVMOperation { /** * 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 + * broadcast tx. See {@link EVMOperation.resolveWalletSender} 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 @@ -133,7 +140,7 @@ export class TransferAdmin extends EVMOperation { chain: EVMChain, params: EVMExecuteParams, ): Promise { - const sender = await this.senderBoundToWallet(params.wallet, params.sender) + const sender = await this.resolveWalletSender(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 index 962af9a42..06c0e0e3c 100644 --- a/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts @@ -83,8 +83,7 @@ describe('pool versions', () => { 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". + // `1.6.0` is a real on-chain string, but no ABI is vendored for it — deferred, not unknown assert.equal(isTokenPoolVersion('1.6.0'), false) assert.equal(isTokenPoolVersion('garbage'), false) }) @@ -230,4 +229,75 @@ describe('resolveEncoder', () => { CCTOperationUnsupportedError, ) }) + + it('inherits the lower version\u2019s encoder across every absent key above it', () => { + // a single V1_5_0 entry \u2014 the shape `transfer-ownership.ts` uses \u2014 must cover every version + const encoders = { [TokenPoolVersion.V1_5_0]: () => 'only' } + for (const version of Object.values(TokenPoolVersion)) + assert.equal(resolveEncoder(encoders, version, 'op')(), 'only') + }) + + it('stops at an explicit null ceiling instead of inheriting the encoder downward', () => { + // `applyAllowListUpdates`: present 1.5.0\u20131.6.1, removed outright in 2.0.0 + const encoders = { + [TokenPoolVersion.V1_5_0]: () => 'allowList', + [TokenPoolVersion.V2_0_0]: null, + } + assert.equal(resolveEncoder(encoders, TokenPoolVersion.V1_5_0, 'op')(), 'allowList') + assert.equal(resolveEncoder(encoders, TokenPoolVersion.V1_5_1, 'op')(), 'allowList') + assert.equal(resolveEncoder(encoders, TokenPoolVersion.V1_6_1, 'op')(), 'allowList') + assert.throws( + () => resolveEncoder(encoders, TokenPoolVersion.V2_0_0, 'op'), + (error: unknown) => + error instanceof CCTOperationUnsupportedError && + error.context.operation === 'op' && + error.context.version === TokenPoolVersion.V2_0_0, + ) + }) + + it('applies a null ceiling to every version at or above it, not just the keyed one', () => { + // a ceiling keyed below the top must not be escaped by asking for a higher version + const encoders = { + [TokenPoolVersion.V1_5_0]: () => 'a', + [TokenPoolVersion.V1_6_1]: null, + } + assert.equal(resolveEncoder(encoders, TokenPoolVersion.V1_5_1, 'op')(), 'a') + assert.throws( + () => resolveEncoder(encoders, TokenPoolVersion.V1_6_1, 'op'), + CCTOperationUnsupportedError, + ) + assert.throws( + () => resolveEncoder(encoders, TokenPoolVersion.V2_0_0, 'op'), + CCTOperationUnsupportedError, + ) + }) + + it('lets a later version re-register an encoder above a null ceiling', () => { + // the walk is downward-from-requested, so a re-added function is found before the ceiling + const encoders = { + [TokenPoolVersion.V1_5_0]: () => 'old', + [TokenPoolVersion.V1_5_1]: null, + [TokenPoolVersion.V2_0_0]: () => 'new', + } + assert.equal(resolveEncoder(encoders, TokenPoolVersion.V1_5_0, 'op')(), 'old') + assert.throws( + () => resolveEncoder(encoders, TokenPoolVersion.V1_6_1, 'op'), + CCTOperationUnsupportedError, + ) + assert.equal(resolveEncoder(encoders, TokenPoolVersion.V2_0_0, 'op')(), 'new') + }) + + it('throws when the requested version itself is the only null entry', () => { + assert.throws( + () => resolveEncoder({ [TokenPoolVersion.V1_5_0]: null }, TokenPoolVersion.V1_5_0, 'op'), + CCTOperationUnsupportedError, + ) + }) + + it('throws on an empty table', () => { + assert.throws( + () => resolveEncoder({}, TokenPoolVersion.V1_6_1, 'op'), + CCTOperationUnsupportedError, + ) + }) }) diff --git a/ccip-sdk/src/cct/evm/token-pool/contracts.ts b/ccip-sdk/src/cct/evm/token-pool/contracts.ts index 9b9061397..2be8a8445 100644 --- a/ccip-sdk/src/cct/evm/token-pool/contracts.ts +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.ts @@ -1,19 +1,25 @@ /** - * 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`. + * EVM token-pool contract metadata for CCT, and the reads that resolve it. Families, types and + * versions; the cached {@link Interface}s built from the vendored ABIs; on-chain type/version + * resolution ({@link resolveTokenPool}, {@link parseTokenPoolVersion}, + * {@link getTokenPoolInterface}); the deployable pools' creation artifacts + * ({@link getTokenPoolArtifact}); the version dispatch the write ops encode through + * ({@link resolveEncoder}); and the owner pre-flight they share ({@link assertPoolOwner}). + * Mirrors `token/contracts.ts`. * * @packageDocumentation */ -import { Interface } from 'ethers' +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, CCTOperationUnsupportedError, + CCTParamsInvalidError, } 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' @@ -28,6 +34,7 @@ import BURN_MINT_TOKEN_POOL_V2_0_0_BYTECODE from '../artifacts/bytecode/V2_0_0/b 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' +import { getTypedContract } from '../query.ts' /** * ABI families for pool resolution. The burn-* variants are interface-compatible for CCT @@ -69,10 +76,9 @@ export function isTokenPoolType(v: string): v is TokenPoolType { } /** - * 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. + * Classifies a supported pool type into its ABI {@link TokenPoolFamily} by name: every burn-* pool + * shares the `BurnMint` ABI (hence the anchored `^Burn`, which also covers + * `BurnMintWithLockReleaseFlagTokenPool`), and the rest share `LockRelease`. * {@link TOKEN_POOL_TYPES} is the gate, so only allowlisted, ABI-compatible names reach here. */ export function getTokenPoolFamily(type: TokenPoolType): TokenPoolFamily { @@ -84,9 +90,7 @@ export function isLockReleaseTokenPoolType(type: TokenPoolType): type is LockRel return getTokenPoolFamily(type) === 'LockRelease' } -/** - * Known pool versions, low to high. Value order drives floor-match in {@link resolveEncoder}. - */ +/** 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', @@ -121,6 +125,7 @@ export function parseTokenPoolVersion({ throw new CCTContractTypeInvalidError(address, TOKEN_POOL_TYPES.join(', '), contractType) if (!isTokenPoolVersion(version)) throw new CCTContractVersionUnsupportedError(contractType, version, { context: { address } }) + return { type: contractType, version } } @@ -138,6 +143,49 @@ export async function resolveTokenPool( return parseTokenPoolVersion({ address, contractType, version }) } +/** + * `Ownable2Step.owner()`, declared identically by every supported pool type and version — so one + * vendored ABI reads the owner of any of them, with no per-generation dispatch. + */ +type PoolOwnerGetter = Pick, 'owner'> + +/** + * Pre-flights `sender` against the pool's on-chain `owner()` for an owner-gated write, so an + * unauthorized caller fails as a {@link CCTParamsInvalidError} here instead of as an opaque + * `OwnableUnauthorizedAccount` revert after a multisig has already reviewed and signed. + * + * @remarks A single `owner()` call, not the full `getTokenPoolState` query: `owner` is the only + * field this needs and the only one whose getter never changed spelling, so reading it directly + * costs one `eth_call` instead of a second `typeAndVersion` resolution plus every admin field. + * @remarks For an owner-*only* gate. Not for a gate that accepts more than the owner — + * `setChainRateLimiterConfigs` takes `owner` **or** `rateLimitAdmin`, and collapsing that + * disjunction to this helper would lock out a delegated rate-limit admin. + * @param operation - Operation name, for the error's `operation` field. + * @param chain - Chain to read the owner from. + * @param poolAddress - Token pool being written to. + * @param sender - The address the tx will be sent from; compared checksummed. + * @throws {@link CCTParamsInvalidError} if `sender` is not the pool owner + */ +export async function assertPoolOwner( + operation: string, + chain: EVMChain, + poolAddress: string, + sender: string, +): Promise { + const pool: PoolOwnerGetter = getTypedContract( + chain, + poolAddress, + BURN_MINT_TOKEN_POOL_V1_5_0_ABI, + ) + const owner = getAddress(resultToObject(await pool.owner())) + if (getAddress(sender) === owner) return + throw new CCTParamsInvalidError( + operation, + 'sender', + `must be the current token pool owner (${owner})`, + ) +} + /** * 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` @@ -200,18 +248,33 @@ export function getTokenPoolArtifact(type: DeployableTokenPoolType): DeployArtif } /** - * 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` + * Returns the encoder registered at the greatest version less than or equal to `version`, + * walking {@link TokenPoolVersion} downwards from `version`. + * + * A table entry says one of two things: + * - **absent key** — the calldata did not change here, so it *inherits* the closest lower entry. + * One entry per calldata change therefore covers every higher version. + * - **explicit `null`** — the function is gone from this version up. The walk stops instead of + * inheriting downwards, and the op is reported unsupported. Floor-match alone is only sound for + * functions that survive; without a ceiling, a removed function's older encoder would emit + * calldata for a selector the pool does not implement. + * + * @param encoders - Sparse table keyed by {@link TokenPoolVersion}; `null` marks a removal ceiling. + * @param version - The resolved on-chain pool version to encode for. + * @param op - Operation name, for the error. + * @throws {@link CCTOperationUnsupportedError} if nothing is registered at or below `version`, or + * if the walk hits an explicit `null` ceiling first */ export function resolveEncoder( - encoders: Partial>, + 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]!] + // removed here — do not inherit the lower encoder downward + if (encoder === null) break if (encoder !== undefined) return encoder } throw new CCTOperationUnsupportedError(op, version) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.test.ts new file mode 100644 index 000000000..e0da0dde8 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.test.ts @@ -0,0 +1,927 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError, toBeHex } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { parseTypeAndVersion } from '../../../../utils.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { type TokenPoolFamily, TOKEN_POOL_INTERFACES, TokenPoolVersion } from '../contracts.ts' +import { type ApplyChainUpdatesParams, ApplyChainUpdates } from './apply-chain-updates.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' + '88'.repeat(20) +const NOT_OWNER = '0x' + '99'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +const SEL_A = 16015286601757825753n // ethereum-sepolia +const SEL_B = 3478487238524512106n // arbitrum-sepolia +const REMOTE_TOKEN = '0x' + 'aa'.repeat(20) +const REMOTE_POOL_1 = '0x' + 'bb'.repeat(20) +const REMOTE_POOL_2 = '0x' + 'cc'.repeat(32) // a 32-byte (non-EVM) remote pool + +const INBOUND = { enabled: true, capacity: 100_000n, rate: 167n } as const +const OUTBOUND = { enabled: false } as const + +/** Both directions as the ABI spells them — `isEnabled`, with the disabled amounts defaulted. */ +const ABI_INBOUND = { isEnabled: true, capacity: 100_000n, rate: 167n } +const ABI_OUTBOUND = { isEnabled: false, capacity: 0n, rate: 0n } + +/** + * Expected calldata is built from interfaces declared *here*, from the human-readable signatures + * read off the vendored ABIs — not from the SDK's own cached `TOKEN_POOL_INTERFACES`, which would + * make the parity assertions circular. + */ +const FRESH_V1_5_0 = new Interface([ + 'function applyChainUpdates((uint64 remoteChainSelector, bool allowed, bytes remotePoolAddress, bytes remoteTokenAddress, (bool isEnabled, uint128 capacity, uint128 rate) outboundRateLimiterConfig, (bool isEnabled, uint128 capacity, uint128 rate) inboundRateLimiterConfig)[] chains)', +]) +const FRESH_V1_5_1 = new Interface([ + 'function applyChainUpdates(uint64[] remoteChainSelectorsToRemove, (uint64 remoteChainSelector, bytes[] remotePoolAddresses, bytes remoteTokenAddress, (bool isEnabled, uint128 capacity, uint128 rate) outboundRateLimiterConfig, (bool isEnabled, uint128 capacity, uint128 rate) inboundRateLimiterConfig)[] chainsToAdd)', +]) + +const DATA_V1_5_0 = FRESH_V1_5_0.encodeFunctionData('applyChainUpdates', [ + [ + { + remoteChainSelector: SEL_A, + allowed: true, + remotePoolAddress: REMOTE_POOL_1, + remoteTokenAddress: REMOTE_TOKEN, + outboundRateLimiterConfig: ABI_OUTBOUND, + inboundRateLimiterConfig: ABI_INBOUND, + }, + { + remoteChainSelector: SEL_B, + allowed: false, + remotePoolAddress: REMOTE_POOL_1, + remoteTokenAddress: REMOTE_TOKEN, + outboundRateLimiterConfig: ABI_OUTBOUND, + inboundRateLimiterConfig: ABI_OUTBOUND, + }, + ], +]) + +const DATA_V1_5_1 = FRESH_V1_5_1.encodeFunctionData('applyChainUpdates', [ + [SEL_B], + [ + { + remoteChainSelector: SEL_A, + remotePoolAddresses: [REMOTE_POOL_1, REMOTE_POOL_2], + remoteTokenAddress: REMOTE_TOKEN, + outboundRateLimiterConfig: ABI_OUTBOUND, + inboundRateLimiterConfig: ABI_INBOUND, + }, + ], +]) + +/** The v1.5.0 params whose expected calldata is {@link DATA_V1_5_0}. */ +function paramsV1_5_0(overrides: Record = {}): ApplyChainUpdatesParams { + return { + version: TokenPoolVersion.V1_5_0, + poolAddress: POOL, + sender: OWNER, + chains: [ + { + remoteChainSelector: SEL_A, + allowed: true, + remoteTokenAddress: REMOTE_TOKEN, + remotePoolAddress: REMOTE_POOL_1, + inboundRateLimiterConfig: INBOUND, + outboundRateLimiterConfig: OUTBOUND, + }, + { + remoteChainSelector: SEL_B, + allowed: false, + remoteTokenAddress: REMOTE_TOKEN, + remotePoolAddress: REMOTE_POOL_1, + inboundRateLimiterConfig: OUTBOUND, + outboundRateLimiterConfig: OUTBOUND, + }, + ], + ...overrides, + } +} + +/** The v1.5.1+ params whose expected calldata is {@link DATA_V1_5_1}. */ +function paramsV1_5_1(overrides: Record = {}): ApplyChainUpdatesParams { + return { + version: TokenPoolVersion.V1_5_1, + poolAddress: POOL, + sender: OWNER, + remoteChainSelectorsToRemove: [SEL_B], + chainsToAdd: [ + { + remoteChainSelector: SEL_A, + remoteTokenAddress: REMOTE_TOKEN, + remotePoolAddresses: [REMOTE_POOL_1, REMOTE_POOL_2], + inboundRateLimiterConfig: INBOUND, + outboundRateLimiterConfig: OUTBOUND, + }, + ], + ...overrides, + } +} + +/** Pool contract type reported per ABI family, both of which exist at every supported version. */ +const POOL_TYPE: Record = { + BurnMint: 'BurnMintTokenPool', + LockRelease: 'LockReleaseTokenPool', +} + +/** + * The `owner()`/getter results `GetTokenPoolState` reads, encoded per version generation: v2.0.0 + * folds router + both admin roles into `getDynamicConfig` and adds the finality window, where the + * legacy versions have standalone getters. + */ +function poolStateReads(version: TokenPoolVersion, family: TokenPoolFamily): Map { + const responses = new Map() + const iface = TOKEN_POOL_INTERFACES[family][version] + const add = (fn: string, values: unknown[]) => + responses.set(iface.getFunction(fn)!.selector, iface.encodeFunctionResult(fn, values)) + + add('getToken', [TOKEN]) + add('owner', [OWNER]) + add('getRmnProxy', [RMN_PROXY]) + add('getSupportedChains', [[SEL_A]]) + if (version === TokenPoolVersion.V2_0_0) { + add('getTokenDecimals', [18]) + add('getDynamicConfig', [ROUTER, RATE_LIMIT_ADMIN, FEE_ADMIN]) + add('getAllowedFinalityConfig', [toBeHex(0, 4)]) + if (family === 'LockRelease') add('getLockBox', [LOCKBOX]) + } else { + add('getRouter', [ROUTER]) + add('getRateLimitAdmin', [RATE_LIMIT_ADMIN]) + } + return responses +} + +type Stub = { + chain: EVMChain + /** How many times the op probed `typeAndVersion` — the first RPC any build makes. */ + probes: () => number +} + +/** + * EVMChain stub: reports `typeAndVersion` for the requested family/version and answers the pool's + * own state getters off `eth_call`. Any other getter reverts. + */ +function stubChain( + version: TokenPoolVersion = TokenPoolVersion.V1_5_1, + family: TokenPoolFamily = 'BurnMint', + owner = OWNER, +): Stub { + let probes = 0 + const responses = poolStateReads(version, family) + if (owner !== OWNER) { + const iface = TOKEN_POOL_INTERFACES[family][version] + responses.set( + iface.getFunction('owner')!.selector, + iface.encodeFunctionResult('owner', [owner]), + ) + } + const chain = { + provider: { + call: ({ 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 Promise.resolve(encoded) + }, + }, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: () => { + probes++ + return Promise.resolve(parseTypeAndVersion(`${POOL_TYPE[family]} ${version}`)) + }, + getTokenInfo: () => Promise.resolve({ decimals: 18, symbol: 'TKN', name: 'Token' }), + nextNonce: () => Promise.resolve(0), + rollbackNonce: () => {}, + } as unknown as EVMChain + return { chain, probes: () => probes } +} + +function fakeSigner(waitError?: Error, address = OWNER) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + 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 ApplyChainUpdates() + +/** Every supported pool version, paired with the parameter shape and calldata it expects. */ +const DISPATCH = [ + { + version: TokenPoolVersion.V1_5_0, + params: paramsV1_5_0, + data: DATA_V1_5_0, + otherParams: paramsV1_5_1, + }, + { + version: TokenPoolVersion.V1_5_1, + params: paramsV1_5_1, + data: DATA_V1_5_1, + otherParams: paramsV1_5_0, + }, + { + version: TokenPoolVersion.V1_6_1, + params: paramsV1_5_1, + data: DATA_V1_5_1, + otherParams: paramsV1_5_0, + }, + { + version: TokenPoolVersion.V2_0_0, + params: paramsV1_5_1, + data: DATA_V1_5_1, + otherParams: paramsV1_5_0, + }, +] as const + +describe('ApplyChainUpdates (cct/evm)', () => { + describe('generate', () => { + for (const { version, params, data } of DISPATCH) { + for (const family of ['BurnMint', 'LockRelease'] as const) { + it(`encodes applyChainUpdates for a v${version} ${family} pool`, async () => { + const { chain } = stubChain(version, family) + const unsigned = await op.generate(chain, params()) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, POOL) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, data) + }) + } + + it(`encodes identical calldata for both ABI families at v${version}`, async () => { + const burnMint = await op.generate(stubChain(version, 'BurnMint').chain, params()) + const lockRelease = await op.generate(stubChain(version, 'LockRelease').chain, params()) + assert.equal(burnMint.transactions[0]!.data, lockRelease.transactions[0]!.data) + assert.equal(burnMint.transactions[0]!.data, data) + }) + } + + it('omits from when sender is not supplied, and skips the owner probe', async () => { + const { chain } = stubChain(TokenPoolVersion.V1_5_1, 'BurnMint', NOT_OWNER) + // owner() reports NOT_OWNER, so this only builds because no sender was given to check + const unsigned = await op.generate(chain, paramsV1_5_1({ sender: undefined })) + assert.equal(unsigned.transactions[0]!.from, undefined) + assert.equal(unsigned.transactions[0]!.data, DATA_V1_5_1) + }) + + it('normalises 0x-less and upper-case hex remote addresses', async () => { + const { chain } = stubChain() + const unsigned = await op.generate( + chain, + paramsV1_5_1({ + chainsToAdd: [ + { + remoteChainSelector: SEL_A, + remoteTokenAddress: 'AA'.repeat(20), + remotePoolAddresses: ['0X' + 'BB'.repeat(20), REMOTE_POOL_2], + inboundRateLimiterConfig: INBOUND, + outboundRateLimiterConfig: OUTBOUND, + }, + ], + }), + ) + assert.equal(unsigned.transactions[0]!.data, DATA_V1_5_1) + }) + + it('accepts uint128 max for both rate-limit amounts', async () => { + // the widest legal RateLimiter.Config; rate === capacity, so only a v1.6.1+ pool takes it + const UINT128_MAX = 2n ** 128n - 1n + const unsigned = await op.generate( + stubChain(TokenPoolVersion.V1_6_1).chain, + paramsV1_5_1({ + remoteChainSelectorsToRemove: [], + chainsToAdd: [ + { + ...paramsV1_5_1AddEntry(), + inboundRateLimiterConfig: { + enabled: true, + capacity: UINT128_MAX, + rate: UINT128_MAX, + }, + }, + ], + }), + ) + assert.equal( + unsigned.transactions[0]!.data, + FRESH_V1_5_1.encodeFunctionData('applyChainUpdates', [ + [], + [ + { + remoteChainSelector: SEL_A, + remotePoolAddresses: [REMOTE_POOL_1], + remoteTokenAddress: REMOTE_TOKEN, + outboundRateLimiterConfig: ABI_OUTBOUND, + inboundRateLimiterConfig: { + isEnabled: true, + capacity: UINT128_MAX, + rate: UINT128_MAX, + }, + }, + ], + ]), + ) + }) + + it('rejects a sender that is not the pool owner', async () => { + const { chain } = stubChain(TokenPoolVersion.V1_5_1, 'BurnMint', NOT_OWNER) + await assert.rejects( + () => op.generate(chain, paramsV1_5_1()), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyChainUpdates' && + err.context.param === 'sender', + ) + }) + }) + + describe('validation', () => { + const cases: [string, ApplyChainUpdatesParams][] = [ + ['poolAddress', paramsV1_5_1({ poolAddress: 'not-an-address' })], + ['sender', paramsV1_5_1({ sender: 'not-an-address' })], + ['version', paramsV1_5_1({ version: '1.6.1' })], + ['chainsToAdd', paramsV1_5_1({ chainsToAdd: 'nope' })], + ['remoteChainSelectorsToRemove', paramsV1_5_1({ remoteChainSelectorsToRemove: 'nope' })], + ['chainsToAdd', paramsV1_5_1({ chainsToAdd: [], remoteChainSelectorsToRemove: [] })], + ['remoteChainSelectorsToRemove[0]', paramsV1_5_1({ remoteChainSelectorsToRemove: [1] })], + ['chainsToAdd[0]', paramsV1_5_1({ chainsToAdd: [null] })], + [ + 'chainsToAdd[0].remoteChainSelector', + paramsV1_5_1({ + chainsToAdd: [{ ...paramsV1_5_1AddEntry(), remoteChainSelector: -1n }], + }), + ], + [ + 'chainsToAdd[0].remotePoolAddresses', + paramsV1_5_1({ chainsToAdd: [{ ...paramsV1_5_1AddEntry(), remotePoolAddresses: [] }] }), + ], + [ + 'chainsToAdd[0].remotePoolAddresses[0]', + paramsV1_5_1({ + chainsToAdd: [{ ...paramsV1_5_1AddEntry(), remotePoolAddresses: ['0xabc'] }], + }), + ], + [ + 'chainsToAdd[0].remotePoolAddresses[1]', + paramsV1_5_1({ + chainsToAdd: [ + { + ...paramsV1_5_1AddEntry(), + remotePoolAddresses: [REMOTE_POOL_1, '0X' + 'BB'.repeat(20)], + }, + ], + }), + ], + [ + 'chainsToAdd[0].remoteTokenAddress', + paramsV1_5_1({ chainsToAdd: [{ ...paramsV1_5_1AddEntry(), remoteTokenAddress: '' }] }), + ], + [ + 'chainsToAdd[0].inboundRateLimiterConfig.enabled', + paramsV1_5_1({ + chainsToAdd: [{ ...paramsV1_5_1AddEntry(), inboundRateLimiterConfig: {} }], + }), + ], + [ + 'chainsToAdd[0].outboundRateLimiterConfig.rate', + paramsV1_5_1({ + chainsToAdd: [ + { + ...paramsV1_5_1AddEntry(), + outboundRateLimiterConfig: { enabled: true, capacity: 1n, rate: 2n }, + }, + ], + }), + ], + // ported from the deleted validate.test.ts: a selector must not be accepted just because it + // fits a wider integer type — uint64 is the tighter bound, and uint128 amounts have a + // ceiling of their own + [ + 'remoteChainSelectorsToRemove[0]', + paramsV1_5_1({ remoteChainSelectorsToRemove: [2n ** 64n] }), + ], + [ + 'chainsToAdd[0].remoteChainSelector', + paramsV1_5_1({ + chainsToAdd: [{ ...paramsV1_5_1AddEntry(), remoteChainSelector: 2n ** 64n }], + }), + ], + [ + 'chainsToAdd[0].inboundRateLimiterConfig.capacity', + paramsV1_5_1({ + chainsToAdd: [ + { + ...paramsV1_5_1AddEntry(), + inboundRateLimiterConfig: { enabled: true, capacity: 2n ** 128n, rate: 1n }, + }, + ], + }), + ], + // an enabled config defaults nothing, so an omitted amount is blamed by the bound check + [ + 'chainsToAdd[0].inboundRateLimiterConfig.rate', + paramsV1_5_1({ + chainsToAdd: [ + { + ...paramsV1_5_1AddEntry(), + inboundRateLimiterConfig: { enabled: true, capacity: 1n }, + }, + ], + }), + ], + // a disabled config must be all-zero, and the whole direction is blamed, not one amount + [ + 'chainsToAdd[0].outboundRateLimiterConfig', + paramsV1_5_1({ + chainsToAdd: [ + { + ...paramsV1_5_1AddEntry(), + outboundRateLimiterConfig: { enabled: false, capacity: 1n }, + }, + ], + }), + ], + ['chains', paramsV1_5_0({ chains: [] })], + ['chains[0]', paramsV1_5_0({ chains: ['nope'] })], + [ + 'chains[0].remoteChainSelector', + paramsV1_5_0({ chains: [{ ...paramsV1_5_0Entry(), remoteChainSelector: 1 }] }), + ], + ['chains[0].allowed', paramsV1_5_0({ chains: [{ ...paramsV1_5_0Entry(), allowed: 'yes' }] })], + [ + 'chains[0].remotePoolAddress', + paramsV1_5_0({ chains: [{ ...paramsV1_5_0Entry(), remotePoolAddress: '0x' }] }), + ], + ] + + for (const [param, params] of cases) { + it(`rejects an invalid ${param} before any RPC`, async () => { + const { chain, probes } = stubChain() + await assert.rejects( + () => op.generate(chain, params), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyChainUpdates' && + err.context.param === param, + ) + assert.equal(probes(), 0, 'no RPC should be issued for an invalid param') + }) + } + }) + + /** + * Three guards that each exist because the *un*guarded outcome is worse than a local failure: + * + * - A **hole** survives element validation outright — `.forEach`/`.map` skip holes — so it used + * to reach ethers as `undefined` and surface as a bare `TypeError` with no + * `operation`/`param` context, and only after the `typeAndVersion` probe had been spent. + * - A **`0n` selector** on an added lane is not guarded on-chain: `s_remoteChainSelectors.add(0)` + * succeeds, so the transaction *mines as a success* and leaves a permanently unroutable lane in + * `getSupportedChains()` that a second owner transaction has to remove. + * - A **duplicate** reverts cleanly on-chain, so this one only saves a transaction — but the + * sibling ops (`setChainRateLimiterConfigs`, and `remotePoolAddresses` within a lane) already + * reject it, and consistency across the family is worth more than the one saved revert. + * + * Every rejection asserts `probes() === 0`: a guard that fires *after* the version probe has + * already broken the "fail before RPC" promise, so the counter is the real subject here. + */ + describe('array density, junk selectors and duplicates', () => { + /** `[first, , last]` — length 3, index 1 absent, which every array method skips. */ + function sparse(first: T, last: T): T[] { + const array = [first] + array[2] = last + return array + } + + /** A v1.5.0 lane whose rate limits are both disabled, so `allowed: false` stays legal. */ + const lane = (remoteChainSelector: bigint, allowed: boolean) => ({ + ...paramsV1_5_0Entry(), + remoteChainSelector, + allowed, + inboundRateLimiterConfig: OUTBOUND, + outboundRateLimiterConfig: OUTBOUND, + }) + const add = (remoteChainSelector: bigint) => ({ + ...paramsV1_5_1AddEntry(), + remoteChainSelector, + }) + + const cases: [string, string, ApplyChainUpdatesParams][] = [ + [ + 'a hole in chainsToAdd', + 'chainsToAdd[1]', + paramsV1_5_1({ chainsToAdd: sparse(add(SEL_A), add(SEL_B)) }), + ], + [ + 'a hole in remoteChainSelectorsToRemove', + 'remoteChainSelectorsToRemove[1]', + paramsV1_5_1({ remoteChainSelectorsToRemove: sparse(SEL_A, SEL_B) }), + ], + [ + "a hole in a lane's remotePoolAddresses", + 'chainsToAdd[0].remotePoolAddresses[1]', + paramsV1_5_1({ + chainsToAdd: [ + { + ...paramsV1_5_1AddEntry(), + remotePoolAddresses: sparse(REMOTE_POOL_1, REMOTE_POOL_2), + }, + ], + }), + ], + [ + 'a hole in the v1.5.0 chains array', + 'chains[1]', + paramsV1_5_0({ chains: sparse(lane(SEL_A, true), lane(SEL_B, true)) }), + ], + [ + 'a 0n selector in chainsToAdd', + 'chainsToAdd[0].remoteChainSelector', + paramsV1_5_1({ chainsToAdd: [add(0n)] }), + ], + [ + 'a 0n selector on a v1.5.0 lane being added', + 'chains[0].remoteChainSelector', + paramsV1_5_0({ chains: [lane(0n, true)] }), + ], + [ + 'a repeated selector in chainsToAdd', + 'chainsToAdd[1].remoteChainSelector', + paramsV1_5_1({ chainsToAdd: [add(SEL_A), add(SEL_A)] }), + ], + [ + 'a repeated selector in remoteChainSelectorsToRemove', + 'remoteChainSelectorsToRemove[1]', + paramsV1_5_1({ remoteChainSelectorsToRemove: [SEL_A, SEL_A] }), + ], + [ + 'a repeated selector in the v1.5.0 chains array', + 'chains[1].remoteChainSelector', + paramsV1_5_0({ chains: [lane(SEL_A, true), lane(SEL_A, false)] }), + ], + ] + + for (const [name, param, params] of cases) { + it(`rejects ${name} before any RPC`, async () => { + const { chain, probes } = stubChain() + await assert.rejects( + () => op.generate(chain, params), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyChainUpdates' && + err.context.param === param, + ) + assert.equal(probes(), 0, `${name} must fail before the typeAndVersion probe`) + }) + } + + // The over-rejection side. Each of these is a legitimate call that the guards above must not + // swallow, and each is the *only* way to express its intent. + it('accepts a 0n selector in remoteChainSelectorsToRemove, so a polluted pool can be repaired', async () => { + const { chain } = stubChain() + const unsigned = await op.generate( + chain, + paramsV1_5_1({ remoteChainSelectorsToRemove: [0n], chainsToAdd: [] }), + ) + const [removals, adds] = FRESH_V1_5_1.decodeFunctionData( + 'applyChainUpdates', + unsigned.transactions[0]!.data!, + ) + assert.deepEqual([...(removals as bigint[])], [0n]) + assert.equal((adds as unknown[]).length, 0) + }) + + it('accepts a v1.5.0 removal of a 0n lane, where allowed: false is the removal', async () => { + const { chain } = stubChain(TokenPoolVersion.V1_5_0) + const unsigned = await op.generate(chain, paramsV1_5_0({ chains: [lane(0n, false)] })) + const [chains] = FRESH_V1_5_0.decodeFunctionData( + 'applyChainUpdates', + unsigned.transactions[0]!.data!, + ) + const [entry] = chains as [{ remoteChainSelector: bigint; allowed: boolean }] + assert.equal(entry.remoteChainSelector, 0n) + assert.equal(entry.allowed, false) + }) + + it('keeps the wholesale-replace idiom: one selector in both arrays at once', async () => { + const { chain } = stubChain() + const unsigned = await op.generate( + chain, + paramsV1_5_1({ remoteChainSelectorsToRemove: [SEL_A], chainsToAdd: [add(SEL_A)] }), + ) + const [removals, adds] = FRESH_V1_5_1.decodeFunctionData( + 'applyChainUpdates', + unsigned.transactions[0]!.data!, + ) + assert.deepEqual([...(removals as bigint[])], [SEL_A]) + const [entry] = adds as [{ remoteChainSelector: bigint }] + assert.equal(entry.remoteChainSelector, SEL_A) + }) + + it('rejects the zero pool address before any RPC', async () => { + const { chain, probes } = stubChain() + await assert.rejects( + () => op.generate(chain, paramsV1_5_1({ poolAddress: ZeroAddress })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyChainUpdates' && + err.context.param === 'poolAddress', + ) + assert.equal(probes(), 0) + }) + }) + + describe('execute', () => { + it('signs and submits, resolving to the tx hash', async () => { + assert.deepEqual( + await op.execute(stubChain().chain, { + ...paramsV1_5_1({ sender: undefined }), + wallet: fakeSigner(), + }), + { hash: HASH }, + ) + }) + + it('maps an on-chain revert to CCIPExecTxRevertedError', async () => { + await assert.rejects( + () => + op.execute(stubChain().chain, { + ...paramsV1_5_1({ sender: undefined }), + wallet: fakeSigner(makeError('execution reverted', 'CALL_EXCEPTION')), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && err.context.operation === 'applyChainUpdates', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain().chain, { ...paramsV1_5_1(), wallet: {} }), + CCIPWalletInvalidError, + ) + }) + + it('rejects a sender that is not the executing wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain().chain, { + ...paramsV1_5_1({ sender: NOT_OWNER }), + wallet: fakeSigner(), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyChainUpdates' && + err.context.param === 'sender', + ) + }) + + it('rejects a wallet that is not the pool owner', async () => { + await assert.rejects( + () => + op.execute(stubChain(TokenPoolVersion.V1_5_1, 'BurnMint', NOT_OWNER).chain, { + ...paramsV1_5_1({ sender: undefined }), + wallet: fakeSigner(), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyChainUpdates' && + err.context.param === 'sender', + ) + }) + }) + + /** + * v1.5.0's `applyChainUpdates` validates BOTH directions with + * `RateLimiter._validateTokenBucketConfig(config, mustBeDisabled: !update.allowed)`, which + * reverts `RateLimitMustBeDisabled()` when `isEnabled && mustBeDisabled`. A removal carrying a + * lane's current (enabled) limits — the obvious way to write one, by reading the lane back and + * flipping `allowed` — therefore always reverts, so it has to fail locally instead. + * + * v1.5.1+ has no such rule: removals there are a separate `remoteChainSelectorsToRemove` array + * and the shape has no `allowed` bit at all, so there is nothing to apply it to. + */ + describe('v1.5.0 lane removal requires both rate limits disabled', () => { + const removal = (overrides: Record) => + paramsV1_5_0({ + chains: [{ ...paramsV1_5_0Entry(), allowed: false, ...overrides }], + }) + + for (const direction of ['inboundRateLimiterConfig', 'outboundRateLimiterConfig'] as const) { + it(`rejects allowed: false with an enabled ${direction}, before any RPC`, async () => { + const { chain, probes } = stubChain(TokenPoolVersion.V1_5_0) + await assert.rejects( + () => + op.generate( + chain, + removal({ + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: false }, + [direction]: { enabled: true, capacity: 100_000n, rate: 167n }, + }), + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyChainUpdates' && + err.context.param === `chains[0].${direction}`, + ) + assert.equal(probes(), 0, 'the rule needs no version, so it must fail before any RPC') + }) + } + + it('accepts allowed: false when both directions are disabled', async () => { + const { chain } = stubChain(TokenPoolVersion.V1_5_0) + const unsigned = await op.generate( + chain, + removal({ + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: false }, + }), + ) + assert.equal(unsigned.transactions[0]!.data!.slice(0, 10), '0xdb6327dc') + }) + + it('does not constrain enabled limits when allowed is true', async () => { + const { chain } = stubChain(TokenPoolVersion.V1_5_0) + const unsigned = await op.generate( + chain, + paramsV1_5_0({ chains: [{ ...paramsV1_5_0Entry(), allowed: true }] }), + ) + assert.equal(unsigned.transactions[0]!.data!.slice(0, 10), '0xdb6327dc') + }) + }) + + /** + * The enabled-bucket rate bound is version-dependent, so it is applied in the encoder (the first + * place the pool version is known) rather than in `validate()`: + * + * - v1.5.0/v1.5.1 revert `InvalidRateLimitRate` unless `0 < rate < capacity`. + * - v1.6.1/v2.0.0 only revert on `rate > capacity`, so `rate === capacity` and `rate === 0n` are + * legitimate — the accept-side cases below exist so nobody tightens the rule globally. + */ + describe('version-specific rate-limit bounds', () => { + const STRICT_CASES = [ + { label: 'rate === capacity', limit: { enabled: true, capacity: 10n, rate: 10n } }, + { label: 'a zero rate', limit: { enabled: true, capacity: 10n, rate: 0n } }, + ] as const + + for (const { label, limit } of STRICT_CASES) { + it(`rejects ${label} on a v1.5.0 pool`, async () => { + await assert.rejects( + () => + op.generate( + stubChain(TokenPoolVersion.V1_5_0).chain, + paramsV1_5_0({ + chains: [{ ...paramsV1_5_0Entry(), inboundRateLimiterConfig: limit }], + }), + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyChainUpdates' && + err.context.param === 'chains[0].inboundRateLimiterConfig.rate', + ) + }) + + it(`rejects ${label} on a v1.5.1 pool`, async () => { + await assert.rejects( + () => + op.generate( + stubChain(TokenPoolVersion.V1_5_1).chain, + paramsV1_5_1({ + chainsToAdd: [{ ...paramsV1_5_1AddEntry(), inboundRateLimiterConfig: limit }], + }), + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyChainUpdates' && + err.context.param === 'chainsToAdd[0].inboundRateLimiterConfig.rate', + ) + }) + + for (const version of [TokenPoolVersion.V1_6_1, TokenPoolVersion.V2_0_0] as const) { + it(`accepts ${label} on a v${version} pool`, async () => { + const unsigned = await op.generate( + stubChain(version).chain, + paramsV1_5_1({ + remoteChainSelectorsToRemove: [], + chainsToAdd: [{ ...paramsV1_5_1AddEntry(), inboundRateLimiterConfig: limit }], + }), + ) + assert.equal( + unsigned.transactions[0]!.data, + FRESH_V1_5_1.encodeFunctionData('applyChainUpdates', [ + [], + [ + { + remoteChainSelector: SEL_A, + remotePoolAddresses: [REMOTE_POOL_1], + remoteTokenAddress: REMOTE_TOKEN, + outboundRateLimiterConfig: ABI_OUTBOUND, + inboundRateLimiterConfig: { + isEnabled: true, + capacity: limit.capacity, + rate: limit.rate, + }, + }, + ], + ]), + ) + }) + } + } + + it('still rejects rate > capacity on a v2.0.0 pool', async () => { + await assert.rejects( + () => + op.generate( + stubChain(TokenPoolVersion.V2_0_0).chain, + paramsV1_5_1({ + chainsToAdd: [ + { + ...paramsV1_5_1AddEntry(), + inboundRateLimiterConfig: { enabled: true, capacity: 10n, rate: 11n }, + }, + ], + }), + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'chainsToAdd[0].inboundRateLimiterConfig.rate', + ) + }) + }) + + describe('version dispatch', () => { + for (const { version, params, data, otherParams } of DISPATCH) { + const shape = version === TokenPoolVersion.V1_5_0 ? 'chains[]' : 'add/remove' + + it(`picks the ${shape} encoder for a v${version} pool`, async () => { + const unsigned = await op.generate(stubChain(version).chain, params()) + assert.equal(unsigned.transactions[0]!.data, data) + // the two signatures have different selectors, so this pins the encoder, not just the args + assert.equal( + unsigned.transactions[0]!.data.slice(0, 10), + version === TokenPoolVersion.V1_5_0 ? '0xdb6327dc' : '0xe8a1da17', + ) + }) + + it(`rejects the wrong declared version for a v${version} pool`, async () => { + await assert.rejects( + () => op.generate(stubChain(version).chain, otherParams()), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyChainUpdates' && + err.context.param === 'version', + ) + }) + } + }) +}) + +/** One valid v1.5.1 addition, to spread invalid fields over. */ +function paramsV1_5_1AddEntry() { + return { + remoteChainSelector: SEL_A, + remoteTokenAddress: REMOTE_TOKEN, + remotePoolAddresses: [REMOTE_POOL_1], + inboundRateLimiterConfig: INBOUND, + outboundRateLimiterConfig: OUTBOUND, + } +} + +/** One valid v1.5.0 lane update, to spread invalid fields over. */ +function paramsV1_5_0Entry() { + return { + remoteChainSelector: SEL_A, + allowed: true, + remoteTokenAddress: REMOTE_TOKEN, + remotePoolAddress: REMOTE_POOL_1, + inboundRateLimiterConfig: INBOUND, + outboundRateLimiterConfig: OUTBOUND, + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.ts b/ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.ts new file mode 100644 index 000000000..8256f970c --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.ts @@ -0,0 +1,561 @@ +/** + * applyChainUpdates — configures, enables and disables a token pool's remote lanes: the remote + * token, the remote pool(s) allowed to bridge into it, and both directional rate limits. + * + * The one CCT pool write whose *parameters* changed shape mid-life, so it is discriminated on + * {@link ApplyChainUpdatesParams.version} rather than version-transparent, and sectioned by version + * so each shape's type, parser and encoder sit together. + * + * @packageDocumentation + */ + +import type { Interface } 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 { + parseHexBytes, + parseRecord, + parseUniqueHexBytesArray, + validateArray, + validateBoolean, + validateNonZeroAddress, + validateUint128, + validateUint64, +} from '../../validate.ts' +import { + TokenPoolVersion, + assertPoolOwner, + getTokenPoolInterface, + resolveEncoder, + resolveTokenPool, +} from '../contracts.ts' + +// --------------------------------------------------------------------------- +// Shared +// --------------------------------------------------------------------------- + +/** + * The `version` discriminant of {@link ApplyChainUpdatesParams}: the two parameter shapes + * `applyChainUpdates` has had, each spelled as the version that introduced it — so `1.5.1` is the + * shape for every pool from v1.5.1 up, v1.6.1 and v2.0.0 included. + */ +export type ApplyChainUpdatesParamVersion = + | typeof TokenPoolVersion.V1_5_0 + | typeof TokenPoolVersion.V1_5_1 + +/** + * One direction of a token pool rate limiter, as callers write it. Amounts are in the token's + * smallest unit: at 6 decimals, `1_000_000n` is one token. + * + * @remarks Field-for-field the Solana `RateLimitConfig` in + * `cct/solana/token-pool/operations/set-chain-rate-limit.ts`, so cross-family callers write one + * shape; only the bound differs (EVM `uint128`, Solana `u64`). Hence the discriminant is spelled + * **`enabled`** rather than the ABI's `isEnabled`; {@link parseRateLimitConfig} resolves it to + * {@link RateLimitConfig}. Distinct from the read-side `RateLimiterState` in `chain.ts`, which also + * reports the live `tokens` balance. + */ +export type RateLimitConfigInput = + | { + /** Whether this directional rate limit is enforced. */ + enabled: true + /** Maximum token amount in the bucket (`uint128`); must be at least `rate`. */ + capacity: bigint + /** + * Token amount restored to the bucket per second (`uint128`); at most `capacity`, which + * v1.5.0/v1.5.1 tighten to `0 < rate < 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 + } + +/** + * One direction of a rate limiter as the ABI spells it: a {@link RateLimitConfigInput} with its + * optional amounts resolved to concrete `bigint`s and its discriminant re-keyed. A parsed lane is + * therefore a `ChainUpdate` struct verbatim, so the encoders need no re-keying pass. + */ +export type RateLimitConfig = { + /** Whether this directional rate limit is enforced (the ABI's spelling of `enabled`). */ + isEnabled: boolean + /** Maximum token amount in the bucket (`uint128`); zero when disabled. */ + capacity: bigint + /** Token amount restored to the bucket per second (`uint128`); zero when disabled. */ + rate: bigint +} + +/** + * Validates one direction of a rate limiter and fills in its omitted amounts. Every rule here is + * version-independent, so it runs before the first RPC; the version-conditional + * `0 < rate < capacity` bound waits for {@link ApplyChainUpdates.assertRateBounds}. `direction` is + * this direction's param path, so failures report as `${direction}.rate`. + * @throws {@link CCTParamsInvalidError} if `config` is not a valid rate-limit configuration + */ +function parseRateLimitConfig( + operation: string, + direction: string, + config: unknown, +): RateLimitConfig { + const input = parseRecord(operation, direction, config, 'rate-limit configuration') + const { enabled } = input + validateBoolean(operation, `${direction}.enabled`, enabled) + + // Only a disabled direction defaults: an enabled one must state both amounts, so an omitted + // amount stays `undefined` and is rejected by the uint128 check below, under its own path. + const capacity = !enabled && input.capacity === undefined ? 0n : input.capacity + const rate = !enabled && input.rate === undefined ? 0n : input.rate + validateUint128(operation, `${direction}.capacity`, capacity) + validateUint128(operation, `${direction}.rate`, rate) + + 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 { isEnabled: enabled, capacity, rate } +} + +/** The lane fields both parameter shapes share, and which encode identically. */ +type ChainUpdateCommon = { + /** CCIP selector of the remote chain (`uint64`). */ + remoteChainSelector: bigint + /** Hex-encoded remote token address, `0x` prefix optional; must be non-empty whole bytes. */ + remoteTokenAddress: string + /** Rate limit for tokens received from the remote chain. */ + inboundRateLimiterConfig: RateLimitConfigInput + /** Rate limit for tokens sent to the remote chain. */ + outboundRateLimiterConfig: RateLimitConfigInput +} + +/** The top-level parameters both shapes share; each version adds its own lane arrays. */ +type ApplyChainUpdatesBaseParams = { + /** Token pool whose lanes are being configured. */ + poolAddress: string + /** + * Pool owner; sets `tx.from` for offline / multisig signing. When supplied it is also + * pre-flighted against the pool's on-chain `owner()`, so an unauthorized caller fails here + * rather than as an opaque revert. + */ + sender?: string +} + +/** A lane with its rate limits resolved — derived, so the parsed and public shapes cannot drift. */ +type WithParsedRateLimits = Omit & { + inboundRateLimiterConfig: RateLimitConfig + outboundRateLimiterConfig: RateLimitConfig +} + +/** + * Parses a lane's `remoteChainSelector`: a `uint64`, unique within its own array, and — for a lane + * being *added* — non-zero. `seen` is mutated as each selector is accepted, and is per-array: the + * same selector in both v1.5.1 arrays is the replace idiom. + * + * @remarks `requireNonZero` holds only for an addition, which `TokenPool.applyChainUpdates` does + * not guard: `s_remoteChainSelectors.add(0)` succeeds, so the tx **mines as a success** and leaves + * `getSupportedChains()` holding a lane nothing can route. A removal is how such a pool is + * repaired, so `0n` stays legal there. Not a *known*-selector check, though — the registry lags new + * chains, and rejecting a real-but-unrecognised selector is the worse failure. + */ +function parseLaneSelector( + operation: string, + param: string, + selector: unknown, + seen: Set, + requireNonZero: boolean, +): bigint { + validateUint64(operation, param, selector) + if (requireNonZero && selector === 0n) { + throw new CCTParamsInvalidError( + operation, + param, + 'must not be zero: 0 is not a CCIP chain selector, and the pool would accept it as a permanently unroutable lane rather than reverting', + ) + } + if (seen.has(selector)) { + throw new CCTParamsInvalidError( + operation, + param, + `is a duplicate of an earlier entry in the same array (${selector}); each lane may appear only once`, + ) + } + seen.add(selector) + return selector +} + +/** Parses the lane fields both shapes share, in the order failures should be reported. */ +function parseLaneCommon( + operation: string, + path: string, + update: { [k: string]: unknown }, + seen: Set, + requireNonZero: boolean, +): WithParsedRateLimits { + return { + remoteChainSelector: parseLaneSelector( + operation, + `${path}.remoteChainSelector`, + update.remoteChainSelector, + seen, + requireNonZero, + ), + remoteTokenAddress: parseHexBytes( + operation, + `${path}.remoteTokenAddress`, + update.remoteTokenAddress, + ), + inboundRateLimiterConfig: parseRateLimitConfig( + operation, + `${path}.inboundRateLimiterConfig`, + update.inboundRateLimiterConfig, + ), + outboundRateLimiterConfig: parseRateLimitConfig( + operation, + `${path}.outboundRateLimiterConfig`, + update.outboundRateLimiterConfig, + ), + } +} + +// --------------------------------------------------------------------------- +// v1.5.0 +// --------------------------------------------------------------------------- + +/** + * One lane's configuration on a **v1.5.0** pool. + * @remarks Field-for-field the Solana `ChainUpdate` in + * `cct/solana/token-pool/operations/apply-chain-updates.ts`, minus its Solana-only + * `remoteTokenDecimals`. + */ +export type ChainUpdateV1_5_0 = ChainUpdateCommon & { + /** + * Whether the lane is enabled. **v1.5.0 only** — `false` removes the lane, which is how this + * version spells v1.5.1+'s `remoteChainSelectorsToRemove`. Every other field is still required + * and still encoded for a removal, and both rate limits must be `{ enabled: false }`: v1.5.0 + * validates them with `mustBeDisabled = !update.allowed` and reverts `RateLimitMustBeDisabled()` + * otherwise, so passing a lane's current (enabled) limits back through is rejected. + */ + allowed: boolean + /** Hex-encoded remote pool address, `0x` prefix optional. Singular at v1.5.0 — one pool per lane. */ + remotePoolAddress: string +} + +/** {@link ApplyChainUpdatesParamsV1_5_0} once parsed — derived, so the two cannot drift. */ +type ParsedApplyChainUpdatesParamsV1_5_0 = Omit & { + chains: WithParsedRateLimits[] +} + +/** + * Parses the v1.5.0 `chains` array. See {@link ChainUpdateV1_5_0.allowed} for why a removal must + * also carry both rate limits disabled. + */ +function parseChainsV1_5_0(operation: string, chains: unknown) { + validateArray(operation, 'chains', chains, 1) + const seen = new Set() + return chains.map((entry, i) => { + const path = `chains[${i}]` + const update = parseRecord(operation, path, entry, 'chain update') + const { allowed } = update + validateBoolean(operation, `${path}.allowed`, allowed) + const lane = { + ...parseLaneCommon(operation, path, update, seen, allowed), + allowed, + remotePoolAddress: parseHexBytes( + operation, + `${path}.remotePoolAddress`, + update.remotePoolAddress, + ), + } + const stillEnabled = + !allowed && + (['inboundRateLimiterConfig', 'outboundRateLimiterConfig'] as const).find( + (direction) => lane[direction].isEnabled, + ) + if (stillEnabled) { + throw new CCTParamsInvalidError( + operation, + `${path}.${stillEnabled}`, + 'must be disabled when allowed is false: v1.5.0 validates both rate limits with mustBeDisabled = !allowed and reverts RateLimitMustBeDisabled — pass { enabled: false } for a removal', + ) + } + return lane + }) +} + +/** Encodes the v1.5.0 signature: one `chains` array, each lane carrying its own `allowed` bit. */ +const encodeV1_5_0 = ( + iface: Interface, + params: ParsedApplyChainUpdatesParamsV1_5_0, +): UnsignedEVMTx => + callTx(params.poolAddress, iface.encodeFunctionData('applyChainUpdates', [params.chains])) + +// --------------------------------------------------------------------------- +// v1.5.1+ +// --------------------------------------------------------------------------- + +/** + * One lane's configuration on a **v1.5.1+** pool. No `allowed` bit: removals are a separate array + * on {@link ApplyChainUpdatesParams}. + */ +export type ChainUpdateV1_5_1 = ChainUpdateCommon & { + /** + * Hex-encoded remote pool addresses, `0x` prefix optional — plural, because a lane may accept + * several remote pools, e.g. while migrating one. Non-empty, and unique within the lane + * (compared as bytes, so `0xAB` and `ab` collide). + */ + remotePoolAddresses: string[] +} + +/** {@link ApplyChainUpdatesParamsV1_5_1} once parsed — derived, so the two cannot drift. */ +type ParsedApplyChainUpdatesParamsV1_5_1 = Omit & { + chainsToAdd: WithParsedRateLimits[] +} + +/** Parses the v1.5.1+ pair of arrays: removals (applied first on-chain), then additions. */ +function parseChainsV1_5_1( + operation: string, + chainsToAdd: unknown, + remoteChainSelectorsToRemove: unknown, +) { + validateArray(operation, 'chainsToAdd', chainsToAdd) + validateArray(operation, 'remoteChainSelectorsToRemove', remoteChainSelectorsToRemove) + if (!chainsToAdd.length && !remoteChainSelectorsToRemove.length) { + throw new CCTParamsInvalidError( + operation, + 'chainsToAdd', + 'at least one of chainsToAdd or remoteChainSelectorsToRemove must be non-empty', + ) + } + + const seenRemovals = new Set() + const removals = remoteChainSelectorsToRemove.map((selector, i) => + parseLaneSelector( + operation, + `remoteChainSelectorsToRemove[${i}]`, + selector, + seenRemovals, + false, + ), + ) + + const seenAdds = new Set() + const adds = chainsToAdd.map((entry, i) => { + const path = `chainsToAdd[${i}]` + const update = parseRecord(operation, path, entry, 'chain update') + return { + ...parseLaneCommon(operation, path, update, seenAdds, true), + remotePoolAddresses: parseUniqueHexBytesArray( + operation, + `${path}.remotePoolAddresses`, + update.remotePoolAddresses, + ), + } + }) + return { chainsToAdd: adds, remoteChainSelectorsToRemove: removals } +} + +/** Encodes the v1.5.1+ signature: removals first, then the lanes to add. */ +const encodeV1_5_1 = ( + iface: Interface, + params: ParsedApplyChainUpdatesParamsV1_5_1, +): UnsignedEVMTx => + callTx( + params.poolAddress, + iface.encodeFunctionData('applyChainUpdates', [ + params.remoteChainSelectorsToRemove, + params.chainsToAdd, + ]), + ) + +/** + * Parameters for {@link ApplyChainUpdates}, discriminated on `version` — the calldata shape you are + * writing, not a free-form pool version; see {@link ApplyChainUpdatesParamVersion}. + * + * The two signatures have different selectors (`0xdb6327dc` vs `0xe8a1da17`), so + * {@link ApplyChainUpdates.buildUnsigned} checks the declaration against the pool's own + * `typeAndVersion`: a mismatch is a parameter error rather than a tx that reverts on an unknown + * function. + */ +export type ApplyChainUpdatesParams = ApplyChainUpdatesParamsV1_5_0 | ApplyChainUpdatesParamsV1_5_1 + +/** The **v1.5.0** parameter shape: a single `chains` array, each lane carrying its `allowed` bit. */ +export type ApplyChainUpdatesParamsV1_5_0 = ApplyChainUpdatesBaseParams & { + version: typeof TokenPoolVersion.V1_5_0 + /** + * Lanes to configure; `allowed: false` removes one. At least one entry, no holes, and a given + * `remoteChainSelector` may appear only once. + */ + chains: ChainUpdateV1_5_0[] +} + +/** The **v1.5.1+** parameter shape: additions and removals as two arrays. */ +export type ApplyChainUpdatesParamsV1_5_1 = ApplyChainUpdatesBaseParams & { + version: typeof TokenPoolVersion.V1_5_1 + /** + * Lanes to add or reconfigure. To replace a lane's remote pools wholesale, list its selector + * here *and* in `remoteChainSelectorsToRemove` — the contract applies removals first, so that + * cross-array pairing stays legal. Within this array a selector may appear only once, and may + * not be `0n`; holes are rejected too. + */ + chainsToAdd: ChainUpdateV1_5_1[] + /** + * Lanes to remove, applied before `chainsToAdd`. No duplicates and no holes; `0n` *is* accepted + * here, so a pool already holding a junk lane can be cleaned up. + */ + remoteChainSelectorsToRemove: bigint[] +} + +/** + * {@link ApplyChainUpdatesParams} as {@link ApplyChainUpdates.parse} leaves it: selectors + * range-checked, remote addresses normalised to lower-case `0x` hex, and rate limits resolved to + * concrete amounts keyed as the ABI spells them. The encoders add no validation of their own — a + * parsed lane is already a `ChainUpdate` struct, so they only choose the argument order. + */ +type ParsedApplyChainUpdatesParams = + | ParsedApplyChainUpdatesParamsV1_5_0 + | ParsedApplyChainUpdatesParamsV1_5_1 + +/** Encodes parsed params into `applyChainUpdates` calldata, widened over the parsed union. */ +type Encoder = (iface: Interface, params: ParsedApplyChainUpdatesParams) => UnsignedEVMTx + +/** One {@link ApplyChainUpdates.encoders} entry: the shape it accepts, and the encoder for it. */ +type EncoderEntry = { shape: ApplyChainUpdatesParamVersion; encode: Encoder } + +/** + * Configures, enables and disables a token pool's remote lanes via `applyChainUpdates`. + * + * @remarks Owner-gated on-chain (`onlyOwner`). Supply `sender` to have that checked against the + * pool's `owner()` before a tx is built; {@link ApplyChainUpdates.execute} defaults it to the + * signing wallet, the only address a broadcast tx can satisfy it with. + */ +export class ApplyChainUpdates extends EVMOperation< + ApplyChainUpdatesParams, + ParsedApplyChainUpdatesParams +> { + readonly name = 'applyChainUpdates' + + /** + * Encoder per pool version, floor-matched; v1.6.1 and v2.0.0 inherit v1.5.1's. The cast holds + * only while {@link buildUnsigned} checks `shape` against `params.version` before encoding. + */ + private readonly encoders = { + [TokenPoolVersion.V1_5_0]: { shape: TokenPoolVersion.V1_5_0, encode: encodeV1_5_0 }, + [TokenPoolVersion.V1_5_1]: { shape: TokenPoolVersion.V1_5_1, encode: encodeV1_5_1 }, + } as Partial> + + /** + * Validates the pool address and every lane entry before any RPC, *keeping* what each check + * produced so neither {@link buildUnsigned} nor an encoder re-derives it. Only the + * version-conditional rate bound is left to {@link assertRateBounds}. + * @throws {@link CCTParamsInvalidError} if `version` is unknown, or any lane field is invalid + */ + protected override parse(params: ApplyChainUpdatesParams): ParsedApplyChainUpdatesParams { + validateNonZeroAddress(this.name, 'poolAddress', params.poolAddress) + const version: string = params.version + switch (params.version) { + case TokenPoolVersion.V1_5_0: + return { ...params, chains: parseChainsV1_5_0(this.name, params.chains) } + case TokenPoolVersion.V1_5_1: + return { + ...params, + ...parseChainsV1_5_1(this.name, params.chainsToAdd, params.remoteChainSelectorsToRemove), + } + default: + throw new CCTParamsInvalidError( + this.name, + 'version', + `must be one of ${TokenPoolVersion.V1_5_0}, ${TokenPoolVersion.V1_5_1}, got ${String(version)}`, + ) + } + } + + /** + * Applies the version-conditional rate bound — the only lane rule outside {@link parse}, because + * it needs the version `resolveTokenPool` has just reported. + * + * @remarks On v1.5.0/v1.5.1, `RateLimiter._validateTokenBucketConfig` reverts + * `InvalidRateLimitRate` on `rate >= capacity || rate == 0`, so an enabled bucket needs + * `0 < rate < capacity`. v1.6.1 and v2.0.0 reject only `rate > capacity`, so there + * `rate === capacity` and a zero rate are legitimate and must NOT be rejected. + */ + private assertRateBounds(params: ParsedApplyChainUpdatesParams, version: TokenPoolVersion): void { + if (version !== TokenPoolVersion.V1_5_0 && version !== TokenPoolVersion.V1_5_1) return + const lanes = + params.version === TokenPoolVersion.V1_5_0 + ? params.chains.map((lane, i) => [`chains[${i}]`, lane] as const) + : params.chainsToAdd.map((lane, i) => [`chainsToAdd[${i}]`, lane] as const) + + for (const [path, lane] of lanes) { + for (const direction of ['inboundRateLimiterConfig', 'outboundRateLimiterConfig'] as const) { + const { isEnabled, capacity, rate } = lane[direction] + if (!isEnabled || (rate > 0n && rate < capacity)) continue + throw new CCTParamsInvalidError( + this.name, + `${path}.${direction}.rate`, + `must be greater than zero and strictly less than capacity when enabled on a v${version} pool, which reverts InvalidRateLimitRate otherwise (v1.6.1 and later allow rate == capacity and a zero rate)`, + ) + } + } + } + + /** + * Resolves the pool's type and version, applies the checks that needed it, then encodes. + * @throws {@link CCTParamsInvalidError} if the declared `version` is not this pool's shape, a + * rate limit breaks its enabled-bucket bound, or `sender` is not the pool owner + * @throws {@link CCTContractTypeInvalidError} if the address is not a supported pool type + * @throws {@link CCTContractVersionUnsupportedError} if the pool reports an unknown version + */ + protected async buildUnsigned( + chain: EVMChain, + params: ParsedApplyChainUpdatesParams, + ): Promise { + const { type, version } = await resolveTokenPool(chain, params.poolAddress) + + const { shape, encode } = resolveEncoder(this.encoders, version, this.name) + if (params.version !== shape) + throw new CCTParamsInvalidError( + this.name, + 'version', + `must be '${shape}' for this pool, which reports v${version} — the two signatures have different selectors, so the declared shape would not exist on-chain`, + ) + + this.assertRateBounds(params, version) + + if (params.sender !== undefined) + await assertPoolOwner(this.name, chain, params.poolAddress, params.sender) + + return encode(getTokenPoolInterface(type, version), params) + } + + /** + * Signs and submits as the pool owner, defaulting `sender` to the signing wallet — the only + * address the contract's `onlyOwner` check can pass. See + * {@link EVMOperation.resolveWalletSender} for why a divergent `sender` is rejected. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} 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 index d35b16c97..c65582040 100644 --- 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 @@ -97,7 +97,7 @@ export class DeployTokenPool extends EVMDeployOperation { } /** Validates the constructor params before building init-code. */ - protected validate(params: DeployTokenPoolParams): void { + protected override validate(params: DeployTokenPoolParams): void { if (!isDeployableTokenPoolType(params.type)) throw new CCTParamsInvalidError( this.name, 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 index 7eb1259f0..a71da720f 100644 --- 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 @@ -137,6 +137,60 @@ describe('GetTokenPoolState (cct/evm token-pool query)', () => { assert.equal(state.lockBox, LOCKBOX) }) + it('reads a v2.0.0 siloed pool, reporting every field but the lockbox', async () => { + // no no-arg getter in `reads`: a siloed pool declares getLockBox(uint64) instead + const chain = stubChain({ + typeAndVersion: 'SiloedLockReleaseTokenPool 2.0.0', + family: 'LockRelease', + reads: READS, + }) + + const state = await new GetTokenPoolState().query(chain, { poolAddress: POOL }) + + assert.deepEqual(state, { + poolAddress: POOL, + version: '2.0.0', + type: 'SiloedLockReleaseTokenPool', + token: TOKEN, + tokenDecimals: 18, + router: ROUTER, + owner: OWNER, + rmnProxy: RMN_PROXY, + rateLimitAdmin: RATE_LIMIT_ADMIN, + feeAdmin: FEE_ADMIN, + supportedChains: CHAINS, + finalityDepth: 10, + finalitySafe: false, + }) + // per-lane escrow: no single lockbox, so the field is absent rather than zeroed + assert.ok(!('lockBox' in state)) + }) + + it('never calls getLockBox() on a siloed pool, whose escrow is keyed per remote chain', async () => { + const noArgLockBox = + TOKEN_POOL_INTERFACES.LockRelease[TokenPoolVersion.V2_0_0].getFunction( + 'getLockBox()', + )!.selector + const seen: string[] = [] + const chain = stubChain({ + typeAndVersion: 'SiloedLockReleaseTokenPool 2.0.0', + family: 'LockRelease', + reads: READS, + }) + const provider = chain.provider as unknown as { + call: (tx: { data: string }) => Promise + } + const { call } = provider + provider.call = (tx) => { + seen.push(tx.data.slice(0, 10)) + return call(tx) + } + + await new GetTokenPoolState().query(chain, { poolAddress: POOL }) + + assert.ok(!seen.includes(noArgLockBox), 'getLockBox() is not implemented by a siloed pool') + }) + it('reads router and both admin roles from the single getDynamicConfig call', async () => { let calls = 0 const chain = stubChain({ reads: READS }) @@ -211,7 +265,8 @@ describe('GetTokenPoolState (cct/evm token-pool query)', () => { assert.ok(!('lockBox' in state)) }) - it('reads a siloed pool before v2.0.0, where per-lane escrow does not exist yet', async () => { + it('reads a siloed pool at a legacy version through the legacy reader', async () => { + // The legacy reader only calls getters TokenPool itself declares, so it serves any type. const chain = stubChain({ typeAndVersion: 'SiloedLockReleaseTokenPool 1.6.1', family: 'LockRelease', @@ -221,7 +276,6 @@ describe('GetTokenPoolState (cct/evm token-pool query)', () => { 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') }) @@ -291,41 +345,6 @@ describe('GetTokenPoolState (cct/evm token-pool query)', () => { ) }) - 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 }) 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 index f6f894979..e2fe93d92 100644 --- 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 @@ -12,7 +12,6 @@ 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' @@ -88,10 +87,20 @@ export type LockReleaseTokenPoolStateV2_0_0 = TokenPoolStateCoreV2_0_0 & { } /** - * State of a v2.0.0 pool: `type === 'LockReleaseTokenPool'` adds the `lockBox`, the one field the - * two families do not share. + * State of a v2.0.0 *siloed* lock/release pool — every field its non-siloed sibling reports + * **except** `lockBox`: it escrows per remote chain, so it declares `getLockBox(uint64)` and no + * no-arg `getLockBox()`. Read a lane's escrow with `getLockBox(remoteChainSelector)` against the + * pool directly; this query does not enumerate them. */ -export type TokenPoolStateV2_0_0 = BurnMintTokenPoolStateV2_0_0 | LockReleaseTokenPoolStateV2_0_0 +export type SiloedLockReleaseTokenPoolStateV2_0_0 = TokenPoolStateCoreV2_0_0 & { + type: 'SiloedLockReleaseTokenPool' +} + +/** State of a v2.0.0 pool. Only `type === 'LockReleaseTokenPool'` reports a `lockBox`. */ +export type TokenPoolStateV2_0_0 = + | BurnMintTokenPoolStateV2_0_0 + | LockReleaseTokenPoolStateV2_0_0 + | SiloedLockReleaseTokenPoolStateV2_0_0 /** * Admin state of a token pool: `version === '2.0.0'` gates the roles and finality window that @@ -208,25 +217,19 @@ async function readBurnMintTokenPoolV2_0_0( 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 - */ +/** Reads a v2.0.0 lock/release pool: the shared state, plus `lockBox` for the non-siloed variant. */ 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)', - ) - +): Promise { + // the non-siloed ABI reads a siloed pool too — every shared getter is declared identically, and + // `getLockBox()` is only ever called on the variant that declares it const pool = getTypedContract(chain, poolAddress, LOCK_RELEASE_TOKEN_POOL_V2_0_0_ABI) + + if (type === 'SiloedLockReleaseTokenPool') + return { ...(await readTokenPoolV2_0_0(pool, poolAddress)), type } + const [state, lockBox] = await Promise.all([ readTokenPoolV2_0_0(pool, poolAddress), resultToObject(pool.getLockBox()), @@ -255,9 +258,9 @@ export class GetTokenPoolState extends EVMQuery { } /** Validates the pool and new-owner addresses before any RPC. */ - protected validate({ poolAddress, newOwner }: TransferOwnershipParams): void { + protected override validate({ poolAddress, newOwner }: TransferOwnershipParams): void { validateAddress(this.name, 'poolAddress', poolAddress) validateAddress(this.name, 'newOwner', newOwner) } diff --git a/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts b/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts index 69e2c3d5a..acfa3c20f 100644 --- a/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts +++ b/ccip-sdk/src/cct/evm/token/operations/deploy-token.ts @@ -60,7 +60,7 @@ export class DeployToken extends EVMDeployOperation { readonly name = 'deployToken' /** Validates the constructor params before building init-code. */ - protected validate(params: DeployTokenParams): void { + protected override validate(params: DeployTokenParams): void { validateNonEmptyString(this.name, 'name', params.name) validateNonEmptyString(this.name, 'symbol', params.symbol) validateUint8(this.name, 'decimals', params.decimals) diff --git a/ccip-sdk/src/cct/evm/validate.ts b/ccip-sdk/src/cct/evm/validate.ts index 096e72027..33e80a6d3 100644 --- a/ccip-sdk/src/cct/evm/validate.ts +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -1,6 +1,7 @@ /** - * Shared parameter validators for EVM CCT operations. Throws - * {@link CCTParamsInvalidError} before any RPC so invalid inputs fail fast. + * Generic parameter primitives for EVM CCT ops — one Solidity type or one JS shape each, no domain + * knowledge and no chain access, so every one of them throws {@link CCTParamsInvalidError} before + * the first RPC. Op-specific rules (rate limits, lane shapes) live with their op. * * @packageDocumentation */ @@ -58,6 +59,19 @@ export function validateNonEmptyString(operation: string, param: string, value: ) } +/** + * Asserts `value` is a boolean, narrowing it for callers. + * @throws {@link CCTParamsInvalidError} if `value` is not a boolean + */ +export function validateBoolean( + operation: string, + param: string, + value: unknown, +): asserts value is boolean { + if (typeof value !== 'boolean') + throw new CCTParamsInvalidError(operation, param, 'must be a boolean') +} + /** * Asserts `value` is an integer in `[0, 255]` (a Solidity `uint8`). * @throws {@link CCTParamsInvalidError} if `value` is not such an integer @@ -71,13 +85,45 @@ export function validateUint8(operation: string, param: string, value: unknown): ) } -const UINT64_MAX = BigInt(2) ** BigInt(64) - 1n +/** + * Shared `uintN` range check: the three widths below differ only in their bound and their message, + * so the comparison itself lives here once. + * @throws {@link CCTParamsInvalidError} if `value` is not a `bigint` in `[0, 2^bits − 1]` + */ +function assertUintBits(operation: string, param: string, value: unknown, bits: number): void { + if (typeof value === 'bigint' && value >= 0n && value <= (1n << BigInt(bits)) - 1n) return + throw new CCTParamsInvalidError( + operation, + param, + `must be a bigint in [0, 2^${bits} − 1], got ${String(value)}`, + ) +} /** - * Asserts `value` is a `bigint` in `[0, 2^64 − 1]` (a Solidity `uint64`), narrowing it to - * `bigint` for callers. - * @remarks The width of a CCIP chain selector, so this is the check every `remoteChainSelector` - * param goes through. + * 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 { + assertUintBits(operation, param, value, 256) +} + +/** + * Asserts `value` is a `bigint` in `[0, 2^128 − 1]` (a Solidity `uint128`), narrowing it to + * `bigint` for callers — where {@link validateUint256} returns `void`, because callers that + * default an omitted amount to `0n` hold a `bigint | undefined` this has to resolve. + * @throws {@link CCTParamsInvalidError} if `value` is not such a bigint + */ +export function validateUint128( + operation: string, + param: string, + value: unknown, +): asserts value is bigint { + assertUintBits(operation, param, value, 128) +} + +/** + * Asserts `value` is a `bigint` in `[0, 2^64 − 1]` (a Solidity `uint64`), narrowing it to `bigint` + * for callers. The width of a CCIP chain selector, so every `remoteChainSelector` goes through it. * @throws {@link CCTParamsInvalidError} if `value` is not such a bigint */ export function validateUint64( @@ -85,26 +131,104 @@ export function validateUint64( param: string, value: unknown, ): asserts value is bigint { - if (typeof value === 'bigint' && value >= 0n && value <= UINT64_MAX) return - throw new CCTParamsInvalidError( - operation, - param, - `must be a bigint in [0, 2^64 − 1], got ${String(value)}`, - ) + assertUintBits(operation, param, value, 64) } -/** Largest value representable by a Solidity `uint256`. */ -const UINT256_MAX = BigInt(2) ** BigInt(256) - 1n +/** + * Parses an optionally `0x`-prefixed hex string of whole, non-empty bytes into the 0x-prefixed + * lower-case form ethers encodes as `bytes`. + * @remarks No byte cap, unlike Solana's counterpart: the values this guards are *remote* addresses + * carried as `bytes`, and a remote may be Solana or Aptos (32 bytes) as easily as EVM (20), so a + * length ceiling would only reject valid remotes. + * @returns The value as `0x`-prefixed lower-case hex. + * @throws {@link CCTParamsInvalidError} if `value` is not a non-empty whole-byte hex string + */ +export function parseHexBytes(operation: string, param: string, value: unknown): string { + const hex = typeof value === 'string' ? value.replace(/^0x/i, '').toLowerCase() : '' + if (typeof value !== 'string' || !/^(?:[\da-f]{2})+$/.test(hex)) { + throw new CCTParamsInvalidError( + operation, + param, + `must be a non-empty hex string of whole bytes, got ${String(value)}`, + ) + } + return `0x${hex}` +} /** - * Asserts `value` is a `bigint` in `[0, 2^256 − 1]` (a Solidity `uint256`). - * @throws {@link CCTParamsInvalidError} if `value` is not such a bigint + * Parses `value` as a plain object, returned as an indexable record so a caller can validate + * fields one by one before the value has a type. `kind` names the shape in the failure message, + * e.g. `'chain update'` → `must be a chain update`. + * @remarks Arrays and class instances (`Date`, `Map`, …) are objects too, but are not valid + * here: an array would pass field checks only by accident of key naming, and an instance's + * fields live on the prototype, not the record. + * @throws {@link CCTParamsInvalidError} if `value` is not a non-null, non-array plain object */ -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)}`, - ) +export function parseRecord( + operation: string, + param: string, + value: unknown, + kind: string, +): { [k: string]: unknown } { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new CCTParamsInvalidError(operation, param, `must be a ${kind}`) + } + return value as { [k: string]: unknown } +} + +/** + * Asserts `value` is a dense array of at least `minLength` entries, narrowing it for callers. + * @remarks Holes are rejected explicitly: `forEach`/`map` skip them, so a sparse array would walk + * past every element check and reach ABI encoding as `null` (blamed as e.g. `chainsToAdd[1]`). + * @throws {@link CCTParamsInvalidError} if `value` is not an array, is shorter than `minLength`, + * or is sparse + */ +export function validateArray( + operation: string, + param: string, + value: unknown, + minLength = 0, +): asserts value is unknown[] { + if (!Array.isArray(value) || value.length < minLength) + throw new CCTParamsInvalidError( + operation, + param, + minLength > 0 ? `must be a non-empty array` : 'must be an array', + ) + for (let i = 0; i < value.length; i++) + if (!(i in value)) + throw new CCTParamsInvalidError( + operation, + `${param}[${i}]`, + 'must not be a hole — the array is sparse, and a missing element cannot be encoded', + ) +} + +/** + * Parses a non-empty list of `bytes` values into 0x-prefixed lower-case hex, rejecting duplicates. + * @remarks Duplicates are compared *after* {@link parseHexBytes} normalisation, so `0xAB` and `ab` + * collide the way a Solidity `bytes` set would. + * @returns The values as 0x-prefixed lower-case hex, in input order. + * @throws {@link CCTParamsInvalidError} if the list is empty, not an array, sparse, or holds an + * invalid or duplicate value + */ +export function parseUniqueHexBytesArray( + operation: string, + param: string, + value: unknown, +): string[] { + validateArray(operation, param, value, 1) + const seen = new Set() + return value.map((entry, i) => { + const hex = parseHexBytes(operation, `${param}[${i}]`, entry) + if (seen.has(hex)) { + throw new CCTParamsInvalidError( + operation, + `${param}[${i}]`, + 'must not duplicate an earlier entry in the same array', + ) + } + seen.add(hex) + return hex + }) } diff --git a/ccip-sdk/src/cct/operation.ts b/ccip-sdk/src/cct/operation.ts index 215b53eab..425f249c0 100644 --- a/ccip-sdk/src/cct/operation.ts +++ b/ccip-sdk/src/cct/operation.ts @@ -1,6 +1,7 @@ /** - * Cross-family CCT write contract. {@link Operation} defines the shared - * generate/execute surface; each chain family supplies its own lifecycle base. + * Cross-family CCT write contract: the pre-RPC lifecycle (validate → parse) plus the + * generate/execute surface. Mirrors {@link Query} for reads; families bind `Chain` and supply + * `buildUnsigned`/`execute`. * * @packageDocumentation */ @@ -19,12 +20,35 @@ export type ExecuteParams

= P & { wallet: unknown } /** * Abstract CCT write operation: build unsigned tx(s) with {@link generate}, or * sign and submit with {@link execute}. + * + * @remarks {@link parse} is the default pre-RPC hook: use it when the op normalizes, or when a + * validated value must reach `buildUnsigned` already narrowed. Reach for {@link validate} only + * when the op purely rejects and `Parsed = P`. */ -export abstract class Operation { +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 + + /** + * Reject invalid params before any chain RPC. No-op by default: an op that normalizes as it + * checks does that work in {@link parse} instead, and needs no empty stub here. + */ + protected validate(_params: Params): void {} + + /** + * Normalize validated params for the builder — defaults, conversions, derived values. Identity + * by default, so an op that needs no normalization declares nothing. + */ + protected parse(params: Params): Parsed { + // `as Parsed` alone does not narrow: `Parsed` is a default, not a constraint. + return params as unknown as Parsed + } + + /** {@link validate} then {@link parse} — the single pre-RPC step, before any chain access. */ + protected prepare(params: Params): Parsed { + this.validate(params) + return this.parse(params) + } /** Build unsigned transaction(s); no wallet required. */ abstract generate(chain: Chain, params: Params): Promise /** Sign and submit via `params.wallet`; returns once confirmed. */ diff --git a/ccip-sdk/src/cct/solana/operation.ts b/ccip-sdk/src/cct/solana/operation.ts index 2b1e23504..59698b737 100644 --- a/ccip-sdk/src/cct/solana/operation.ts +++ b/ccip-sdk/src/cct/solana/operation.ts @@ -30,31 +30,7 @@ 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) - } - +> extends Operation, Tx, TransactionResult, Parsed> { /** Build instructions from validated, parsed params. */ protected abstract buildUnsigned(chain: SolanaChain, params: Parsed): Promise From 720cbc9eff8738fb7b9bfe8df4d98038250cfc6b Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:04:10 +0100 Subject: [PATCH 75/87] feat(cct-sdk): Add EVM remote pool ops (#386) * feat(cct-sdk): Add get token pool remotes evm query * feat(cct-sdk): Add generic EVM param validators * feat(cct-sdk): Read siloed lock/release pool state * feat(cct-sdk): Add pool owner pre-flight and encoder removal ceilings * refactor(cct-sdk): Hoist validate/parse lifecycle to the shared Operation base * feat(cct-sdk): Add apply chain updates evm op * fix(cct-sdk): Reject a declared version that is not the pool's calldata shape * feat(cct-sdk): Add EVM remote pool ops * Address PR comments * Fix lint * Fix lint --- ccip-sdk/src/cct/evm/index.ts | 195 ++++++++ .../operations/add-remote-pool.test.ts | 428 ++++++++++++++++++ .../token-pool/operations/add-remote-pool.ts | 128 ++++++ .../operations/remove-remote-pool.test.ts | 391 ++++++++++++++++ .../operations/remove-remote-pool.ts | 131 ++++++ .../operations/set-remote-pool.test.ts | 326 +++++++++++++ .../token-pool/operations/set-remote-pool.ts | 112 +++++ .../operations/transfer-ownership.ts | 4 +- .../src/cct/evm/token-pool/remote-pool.ts | 145 ++++++ 9 files changed, 1858 insertions(+), 2 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/add-remote-pool.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/add-remote-pool.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/remove-remote-pool.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/remove-remote-pool.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/set-remote-pool.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/set-remote-pool.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/remote-pool.ts diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 665bfc17a..735a420c2 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -43,6 +43,11 @@ import { type TransferAdminParams, TransferAdmin, } from './token-admin-registry/operations/transfer-admin.ts' +import { type AddRemotePoolParams, AddRemotePool } from './token-pool/operations/add-remote-pool.ts' +import { + type ApplyChainUpdatesParams, + ApplyChainUpdates, +} from './token-pool/operations/apply-chain-updates.ts' import { type ApplyChainUpdatesParams, ApplyChainUpdates, @@ -61,6 +66,11 @@ import { type GetTokenPoolStateResult, GetTokenPoolState, } from './token-pool/operations/get-token-pool-state.ts' +import { + type RemoveRemotePoolParams, + RemoveRemotePool, +} from './token-pool/operations/remove-remote-pool.ts' +import { type SetRemotePoolParams, SetRemotePool } from './token-pool/operations/set-remote-pool.ts' import { type TransferOwnershipParams, TransferOwnership, @@ -86,6 +96,9 @@ export class EVMTokenManager extends TokenManager { readonly #transferOwnership = new TransferOwnership() readonly #getTokenPoolState = new GetTokenPoolState() readonly #getTokenPoolRemotes = new GetTokenPoolRemotes() + readonly #setRemotePool = new SetRemotePool() + readonly #addRemotePool = new AddRemotePool() + readonly #removeRemotePool = new RemoveRemotePool() readonly #applyChainUpdates = new ApplyChainUpdates() // Lockbox operations @@ -630,6 +643,185 @@ export class EVMTokenManager extends TokenManager { return this.#getTokenPoolRemotes.query(this.chain, opts) } + /** + * Builds an unsigned pool `setRemotePool` tx (for multisig / offline signing), replacing the + * remote pool a lane accepts. + * @remarks **v1.5.0 pools only.** A v1.5.0 pool holds exactly one remote pool per lane, and this + * call overwrites it. v1.5.1 replaced it with the additive `addRemotePool` / `removeRemotePool` + * pair and dropped `setRemotePool` from the ABI, so a v1.5.1, v1.6.1 or v2.0.0 pool throws + * {@link CCTOperationUnsupportedError} — use {@link generateUnsignedAddRemotePool} / + * {@link generateUnsignedRemoveRemotePool} there. No emulation is attempted: replacing a set of + * unknown size is not one transaction. + * @remarks `remotePoolAddress` is the *remote* chain's pool address as raw `bytes` (`0x` prefix + * optional), not an EVM address — a Solana, Aptos or Sui pool address is 32 bytes. + * @remarks Owner-gated on-chain. When `sender` is given it is checked against the pool's current + * `owner` before any calldata is built; omit it to build for a signer that is not known yet. + * @throws {@link CCTParamsInvalidError} if any param is invalid, or `sender` is given and is not + * the pool owner + * @throws {@link CCTOperationUnsupportedError} if the pool is v1.5.1 or newer + * @throws {@link CCTContractTypeInvalidError} if `poolAddress` is not a supported pool type + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedSetRemotePool({ + * poolAddress: '0xPool...', // a v1.5.0 pool + * remoteChainSelector: 5009297550715157269n, // ethereum-mainnet + * remotePoolAddress: '0xRemotePool...', // hex bytes; 32 bytes for a non-EVM remote + * sender: '0xPoolOwner...', + * }) + * ``` + */ + generateUnsignedSetRemotePool(opts: SetRemotePoolParams): Promise { + return this.#setRemotePool.generate(this.chain, opts) + } + + /** + * Replaces the remote pool a v1.5.0 pool accepts on one lane, signing + submitting with + * `opts.wallet`. See {@link generateUnsignedSetRemotePool} for the version range and the + * `remotePoolAddress` encoding. + * @remarks `sender` defaults to the signing wallet, which must be the pool owner; passing a + * different `sender` is rejected rather than signed — build with + * {@link generateUnsignedSetRemotePool} for externally-signed flows. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid, or `sender` is given and is not + * the wallet's address / the pool owner + * @throws {@link CCTOperationUnsupportedError} if the pool is v1.5.1 or newer + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.setRemotePool({ + * poolAddress: '0xPool...', // a v1.5.0 pool + * remoteChainSelector: 5009297550715157269n, + * remotePoolAddress: '0xRemotePool...', + * wallet, // the pool owner + * }) + * ``` + */ + setRemotePool(opts: EVMExecuteParams): Promise { + return this.#setRemotePool.execute(this.chain, opts) + } + + /** + * Builds an unsigned pool `addRemotePool` tx (for multisig / offline signing), authorizing one + * more remote pool on a lane. + * @remarks **v1.5.1, v1.6.1 and v2.0.0 pools.** From v1.5.1 a lane holds a *set* of remote + * pools, which is what makes a zero-downtime remote-side pool upgrade possible: add the new + * pool, drain the old one, then {@link removeRemotePool}. A v1.5.0 pool has no additive + * primitive and throws {@link CCTOperationUnsupportedError} — it only supports the wholesale + * {@link setRemotePool}. + * @remarks `remotePoolAddress` is the *remote* chain's pool address as raw `bytes` (`0x` prefix + * optional), not an EVM address — a Solana, Aptos or Sui pool address is 32 bytes. + * @remarks Pre-checked against the chain: the lane's currently registered remote pools are read + * (scoped to `remoteChainSelector`, one call) and an address already among them is rejected + * locally instead of reverting on-chain. A lane with no configuration yet counts as having none. + * Owner-gated: a given `sender` is checked against the pool's `owner`. + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not the + * pool owner, or `remotePoolAddress` is already registered on that lane + * @throws {@link CCTOperationUnsupportedError} if the pool is v1.5.0 + * @throws {@link CCTContractTypeInvalidError} if `poolAddress` is not a supported pool type + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedAddRemotePool({ + * poolAddress: '0xPool...', + * remoteChainSelector: 16015286601757825753n, // ethereum-testnet-sepolia + * remotePoolAddress: '0xNewRemotePool...', + * sender: '0xPoolOwner...', + * }) + * ``` + */ + generateUnsignedAddRemotePool(opts: AddRemotePoolParams): Promise { + return this.#addRemotePool.generate(this.chain, opts) + } + + /** + * Authorizes an additional remote pool on one lane of a v1.5.1+ pool, signing + submitting with + * `opts.wallet`. See {@link generateUnsignedAddRemotePool} for the version range, the + * `remotePoolAddress` encoding and the duplicate pre-check. + * @remarks `sender` defaults to the signing wallet, which must be the pool owner; passing a + * different `sender` is rejected rather than signed — build with + * {@link generateUnsignedAddRemotePool} for externally-signed flows. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not the + * wallet's address / the pool owner, or `remotePoolAddress` is already registered on that lane + * @throws {@link CCTOperationUnsupportedError} if the pool is v1.5.0 + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.addRemotePool({ + * poolAddress: '0xPool...', + * remoteChainSelector: 16015286601757825753n, + * remotePoolAddress: '0xNewRemotePool...', + * wallet, // the pool owner + * }) + * ``` + */ + addRemotePool(opts: EVMExecuteParams): Promise { + return this.#addRemotePool.execute(this.chain, opts) + } + + /** + * Builds an unsigned pool `removeRemotePool` tx (for multisig / offline signing), + * de-authorizing one remote pool on a lane. + * @remarks **v1.5.1, v1.6.1 and v2.0.0 pools** — the versions where a lane holds a set of remote + * pools. The last step of a remote-side pool upgrade started with {@link addRemotePool}. A + * v1.5.0 pool has no removal primitive and throws {@link CCTOperationUnsupportedError}; its + * single remote pool can only be overwritten via {@link setRemotePool}. + * @remarks `remotePoolAddress` is the *remote* chain's pool address as raw `bytes` (`0x` prefix + * optional), not an EVM address — a Solana, Aptos or Sui pool address is 32 bytes. + * @remarks Pre-checked against the chain: the lane's registered remote pools are read (scoped to + * `remoteChainSelector`, one call) and an address that is not among them is rejected locally + * instead of reverting on-chain. Removing the lane's last remote pool is allowed — the contract + * decides — but a lane with no configuration at all has nothing to remove and is rejected. + * Owner-gated: a given `sender` is checked against the pool's `owner`. + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not the + * pool owner, or `remotePoolAddress` is not registered on that lane + * @throws {@link CCTOperationUnsupportedError} if the pool is v1.5.0 + * @throws {@link CCTContractTypeInvalidError} if `poolAddress` is not a supported pool type + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedRemoveRemotePool({ + * poolAddress: '0xPool...', + * remoteChainSelector: 16015286601757825753n, + * remotePoolAddress: '0xDrainedRemotePool...', + * sender: '0xPoolOwner...', + * }) + * ``` + */ + generateUnsignedRemoveRemotePool(opts: RemoveRemotePoolParams): Promise { + return this.#removeRemotePool.generate(this.chain, opts) + } + + /** + * De-authorizes a remote pool on one lane of a v1.5.1+ pool, signing + submitting with + * `opts.wallet`. See {@link generateUnsignedRemoveRemotePool} for the version range, the + * `remotePoolAddress` encoding and the membership pre-check. + * @remarks `sender` defaults to the signing wallet, which must be the pool owner; passing a + * different `sender` is rejected rather than signed — build with + * {@link generateUnsignedRemoveRemotePool} for externally-signed flows. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not the + * wallet's address / the pool owner, or `remotePoolAddress` is not registered on that lane + * @throws {@link CCTOperationUnsupportedError} if the pool is v1.5.0 + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.removeRemotePool({ + * poolAddress: '0xPool...', + * remoteChainSelector: 16015286601757825753n, + * remotePoolAddress: '0xDrainedRemotePool...', + * wallet, // the pool owner + * }) + * ``` + */ + removeRemotePool(opts: EVMExecuteParams): Promise { + return this.#removeRemotePool.execute(this.chain, opts) + } + /** * Applies the pool's remote-lane configuration, signing + submitting with `opts.wallet`. * @remarks Same version-discriminated params as @@ -779,6 +971,9 @@ export type { GetTokenPoolRemotesParams, GetTokenPoolRemotesResult, } from './token-pool/operations/get-token-pool-remotes.ts' +export type { SetRemotePoolParams } from './token-pool/operations/set-remote-pool.ts' +export type { AddRemotePoolParams } from './token-pool/operations/add-remote-pool.ts' +export type { RemoveRemotePoolParams } from './token-pool/operations/remove-remote-pool.ts' export type { ApplyChainUpdatesParamVersion, ApplyChainUpdatesParams, diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/add-remote-pool.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/add-remote-pool.test.ts new file mode 100644 index 000000000..294775c38 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/add-remote-pool.test.ts @@ -0,0 +1,428 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError, toBeHex, zeroPadValue } from 'ethers' + +import type { TokenPoolRemote } from '../../../../chain.ts' +import { + CCIPExecTxRevertedError, + CCIPTokenPoolChainConfigNotFoundError, + CCIPWalletInvalidError, +} from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { parseTypeAndVersion } from '../../../../utils.ts' +import { CCTOperationUnsupportedError, CCTParamsInvalidError } from '../../../errors.ts' +import { + type TokenPoolType, + TOKEN_POOL_INTERFACES, + TokenPoolVersion, + getTokenPoolFamily, +} from '../contracts.ts' +import { type AddRemotePoolParams, AddRemotePool } from './add-remote-pool.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 LOCKBOX = '0x' + '77'.repeat(20) +const NOT_THE_OWNER = '0x' + '88'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +/** An EVM remote pool, as the caller passes it (hex bytes) and as the chain reader returns it. */ +const REMOTE_POOL = '0x' + '99'.repeat(20) +/** Another remote pool, already registered on the lane in the duplicate tests. */ +const OTHER_REMOTE_POOL = '0x' + 'aa'.repeat(20) + +const SELECTOR = 5009297550715157269n // ethereum-mainnet +const SOLANA_SELECTOR = 16423721717087811551n // solana-devnet + +/** Calldata built from a fresh Interface — never the SDK's cached one, or this proves nothing. */ +const FRESH = new Interface([ + 'function addRemotePool(uint64 remoteChainSelector, bytes remotePoolAddress)', +]) +const expectedData = (remotePoolAddress = REMOTE_POOL, selector = SELECTOR) => + FRESH.encodeFunctionData('addRemotePool', [selector, remotePoolAddress]) + +/** The `getTokenPoolState` getters the owner gate reads, per version generation. */ +function poolReads(version: TokenPoolVersion, type: TokenPoolType, owner: string) { + if (version !== TokenPoolVersion.V2_0_0) + return { + getToken: [TOKEN], + owner: [owner], + getRouter: [ROUTER], + getRmnProxy: [RMN_PROXY], + getRateLimitAdmin: [RATE_LIMIT_ADMIN], + getSupportedChains: [[SELECTOR]], + } + return { + getToken: [TOKEN], + owner: [owner], + getRmnProxy: [RMN_PROXY], + getTokenDecimals: [18], + getSupportedChains: [[SELECTOR]], + getDynamicConfig: [ROUTER, RATE_LIMIT_ADMIN, RATE_LIMIT_ADMIN], + getAllowedFinalityConfig: [toBeHex(0, 4)], + ...(getTokenPoolFamily(type) === 'LockRelease' ? { getLockBox: [LOCKBOX] } : {}), + } +} + +type Calls = { typeAndVersion: number; remotes: Array<[string, bigint | undefined]>; calls: number } + +/** + * EVMChain stub: reports `type`/`version`, answers the owner-gate getters off the pool's own + * Interface, and returns (or throws for) one lane's remotes. + */ +function stubChain({ + type = 'BurnMintTokenPool', + version = TokenPoolVersion.V1_5_1, + owner = OWNER, + remotePools = [] as string[], + remotesError, + seen = { typeAndVersion: 0, remotes: [], calls: 0 }, +}: { + type?: TokenPoolType + version?: TokenPoolVersion + owner?: string + remotePools?: string[] + remotesError?: Error + seen?: Calls +} = {}): EVMChain { + const iface = TOKEN_POOL_INTERFACES[getTokenPoolFamily(type)][version] + const responses = new Map( + Object.entries(poolReads(version, type, owner)).map(([fn, values]) => [ + iface.getFunction(fn)!.selector, + iface.encodeFunctionResult(fn, values), + ]), + ) + const remote: TokenPoolRemote = { + remoteToken: TOKEN, + remotePools, + inboundRateLimiterState: null, + outboundRateLimiterState: null, + } + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + seen.calls++ + 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 Promise.resolve(encoded) + }, + }, + typeAndVersion: () => { + seen.typeAndVersion++ + return Promise.resolve(parseTypeAndVersion(`${type} ${version}`)) + }, + getTokenInfo: () => Promise.resolve({ decimals: 18, symbol: 'TKN', name: 'Token' }), + getTokenPoolRemotes: (tokenPool: string, remoteChainSelector?: bigint) => { + seen.remotes.push([tokenPool, remoteChainSelector]) + return remotesError ? Promise.reject(remotesError) : Promise.resolve({ 'a-network': remote }) + }, + nextNonce: () => Promise.resolve(0), + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(waitError?: Error, address = OWNER) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + 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 AddRemotePool() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddress: REMOTE_POOL, + sender: OWNER, + ...overrides, + }) +} + +/** Versions that declare `addRemotePool`, each with both ABI families. */ +const SUPPORTED = [TokenPoolVersion.V1_5_1, TokenPoolVersion.V1_6_1, TokenPoolVersion.V2_0_0] +const TYPES: TokenPoolType[] = ['BurnMintTokenPool', 'LockReleaseTokenPool'] + +describe('AddRemotePool (cct/evm)', () => { + describe('generate', () => { + for (const version of SUPPORTED) { + for (const type of TYPES) { + it(`encodes addRemotePool(selector, bytes) for a ${type} ${version}`, async () => { + const unsigned = await generate(stubChain({ type, version })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, POOL) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, expectedData()) + }) + } + } + + it('produces identical calldata for both ABI families', async () => { + const [burnMint, lockRelease] = await Promise.all( + TYPES.map(async (type) => (await generate(stubChain({ type }))).transactions[0]!.data), + ) + assert.equal(burnMint, lockRelease) + assert.equal(burnMint, expectedData()) + }) + + it('accepts a remotePoolAddress without the 0x prefix', async () => { + const unsigned = await generate(stubChain(), { + remotePoolAddress: REMOTE_POOL.slice(2).toUpperCase(), + }) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + }) + + it('encodes a 32-byte non-EVM remote pool address as-is', async () => { + const remotePoolAddress = '0x' + 'cd'.repeat(32) + const unsigned = await generate(stubChain({ remotePools: [] }), { + remoteChainSelector: SOLANA_SELECTOR, + remotePoolAddress, + }) + assert.equal(unsigned.transactions[0]!.data, expectedData(remotePoolAddress, SOLANA_SELECTOR)) + }) + + it('omits from, and skips the owner read, when sender is not supplied', async () => { + const seen: Calls = { typeAndVersion: 0, remotes: [], calls: 0 } + const unsigned = await generate(stubChain({ seen }), { sender: undefined }) + assert.equal(unsigned.transactions[0]!.from, undefined) + assert.equal(seen.calls, 0, 'no owner gate without a sender to compare') + }) + + it('scopes the remotes read to the one lane', async () => { + const seen: Calls = { typeAndVersion: 0, remotes: [], calls: 0 } + await generate(stubChain({ seen })) + assert.deepEqual(seen.remotes, [[POOL, SELECTOR]]) + }) + }) + + describe('validation', () => { + for (const [param, value] of [ + ['poolAddress', 'not-an-address'], + // a tx to `0x0` hits no code, so it mines as a successful no-op instead of reverting + ['poolAddress', ZeroAddress], + ['sender', 'not-an-address'], + ['remoteChainSelector', 1 as never], + ['remoteChainSelector', -1n], + ['remoteChainSelector', 2n ** 64n], + ['remotePoolAddress', ''], + ['remotePoolAddress', '0x'], + ['remotePoolAddress', '0xabc'], + ['remotePoolAddress', '0xzz'], + ['remotePoolAddress', 42 as never], + ] as const) { + it(`rejects ${param} = ${String(value)} before any RPC`, async () => { + const seen: Calls = { typeAndVersion: 0, remotes: [], calls: 0 } + await assert.rejects( + () => generate(stubChain({ seen }), { [param]: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'addRemotePool' && + err.context.param === param, + ) + assert.deepEqual([seen.typeAndVersion, seen.remotes.length, seen.calls], [0, 0, 0]) + }) + } + }) + + describe('version dispatch', () => { + it('is unsupported on v1.5.0, which has no additive primitive', async () => { + const seen: Calls = { typeAndVersion: 0, remotes: [], calls: 0 } + await assert.rejects( + () => generate(stubChain({ version: TokenPoolVersion.V1_5_0, seen })), + (err: unknown) => + err instanceof CCTOperationUnsupportedError && + err.context.operation === 'addRemotePool' && + err.context.version === '1.5.0', + ) + // the encoder is resolved off the single typeAndVersion read, before any further RPC + assert.deepEqual([seen.typeAndVersion, seen.remotes.length, seen.calls], [1, 0, 0]) + }) + + for (const version of SUPPORTED) { + it(`encodes on v${version}`, async () => { + const unsigned = await generate(stubChain({ version })) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + }) + } + + it('covers every known pool version', () => { + assert.deepEqual(Object.values(TokenPoolVersion), [TokenPoolVersion.V1_5_0, ...SUPPORTED]) + }) + }) + + describe('pre-transaction validation', () => { + it('rejects a remote pool already registered on the lane', async () => { + await assert.rejects( + () => generate(stubChain({ remotePools: [OTHER_REMOTE_POOL, REMOTE_POOL] })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'addRemotePool' && + err.context.param === 'remotePoolAddress', + ) + }) + + it('rejects a duplicate given as left-padded 32-byte bytes', async () => { + // the chain reader returns decoded 20-byte addresses; the caller may pass either form + await assert.rejects( + () => + generate(stubChain({ remotePools: [REMOTE_POOL] }), { + remotePoolAddress: zeroPadValue(REMOTE_POOL, 32), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'remotePoolAddress', + ) + }) + + it('rejects a duplicate whose registered spelling differs only in case', async () => { + await assert.rejects( + () => generate(stubChain({ remotePools: [REMOTE_POOL.toUpperCase().replace('0X', '0x')] })), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'remotePoolAddress', + ) + }) + + it('falls back to raw byte comparison when the remote family has no registered codec', async () => { + // `decodeAddress` only knows the families whose chain module is loaded (EVM always is); + // for anything else the undecoded hex is compared, so a duplicate is still caught + const bytes = '0x' + 'cd'.repeat(32) + await assert.rejects( + () => + generate(stubChain({ remotePools: [bytes] }), { + remoteChainSelector: SOLANA_SELECTOR, + remotePoolAddress: bytes.toUpperCase().replace('0X', '0x'), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'remotePoolAddress', + ) + }) + + it('allows adding to a lane that holds other remote pools', async () => { + const unsigned = await generate(stubChain({ remotePools: [OTHER_REMOTE_POOL] })) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + }) + + it('treats an unconfigured lane as having no remote pools', async () => { + const chain = stubChain({ + remotesError: new CCIPTokenPoolChainConfigNotFoundError(POOL, POOL, 'a-network'), + }) + const unsigned = await generate(chain) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + }) + + it('propagates any other remotes-read failure', async () => { + const boom = new Error('rpc down') + await assert.rejects(() => generate(stubChain({ remotesError: boom })), boom) + }) + + it('rejects a sender that is not the pool owner', async () => { + await assert.rejects( + () => generate(stubChain({ owner: NOT_THE_OWNER })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'addRemotePool' && + err.context.param === 'sender', + ) + }) + }) + + /** + * Regression guard for the owner gate on a v2.0.0 `SiloedLockReleaseTokenPool`. + * + * A siloed pool escrows per remote chain (`getLockBox(uint64)`, no no-arg `getLockBox()`), but + * it is a fully supported `TokenPoolType` and this op's calldata is perfectly valid against one. + * The gate must therefore admit it on the strength of `owner()` alone, without depending on any + * pool-shape detail that only the non-siloed variant reports. + */ + describe('siloed lock/release pools', () => { + const siloedPool = (owner = OWNER) => + stubChain({ type: 'SiloedLockReleaseTokenPool', version: TokenPoolVersion.V2_0_0, owner }) + + it('builds for a siloed lock/release pool, which getTokenPoolState cannot read', async () => { + const unsigned = await generate(siloedPool()) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, POOL) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, expectedData()) + }) + + it('still rejects a sender that is not the siloed pool owner', async () => { + await assert.rejects( + () => generate(siloedPool(NOT_THE_OWNER)), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'addRemotePool' && + err.context.param === 'sender', + ) + }) + }) + + describe('execute', () => { + const params = { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddress: REMOTE_POOL, + } + + it('signs and submits as the owner, resolving to the tx hash', async () => { + assert.deepEqual(await op.execute(stubChain(), { ...params, wallet: fakeSigner() }), { + hash: HASH, + }) + }) + + it('maps an on-chain revert to CCIPExecTxRevertedError', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + ...params, + wallet: fakeSigner(makeError('execution reverted', 'CALL_EXCEPTION')), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && err.context.operation === 'addRemotePool', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, wallet: {} }), + CCIPWalletInvalidError, + ) + }) + + it('rejects a sender that is not the signing wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, sender: NOT_THE_OWNER, wallet: fakeSigner() }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'addRemotePool' && + err.context.param === 'sender', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/add-remote-pool.ts b/ccip-sdk/src/cct/evm/token-pool/operations/add-remote-pool.ts new file mode 100644 index 000000000..8c53b3885 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/add-remote-pool.ts @@ -0,0 +1,128 @@ +/** + * addRemotePool: authorizes one more remote pool address on a lane (v1.5.1+). + * + * @remarks **v1.5.1 and newer.** v1.5.1 turned a lane's single remote pool into a set — several + * remote pools may be authorized at once, which is how a remote-side pool upgrade is rolled out + * without downtime (add the new pool, drain the old, then `removeRemotePool`). v1.5.0 has no + * additive primitive at all, only the wholesale {@link SetRemotePool}, so this op reports itself + * unsupported there rather than emulating an add as a replace. + * + * @packageDocumentation + */ + +import type { Interface } 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 { + TokenPoolVersion, + assertPoolOwner, + getTokenPoolInterface, + resolveEncoder, + resolveTokenPool, +} from '../contracts.ts' +import { + type ParsedRemotePoolParams, + type RemotePoolParams, + isRegisteredRemotePool, + parseRemotePoolParams, + readRegisteredRemotePools, +} from '../remote-pool.ts' + +/** + * Parameters for {@link AddRemotePool} — see {@link RemotePoolParams}; `remotePoolAddress` is the + * remote chain's pool address as hex bytes, added to the lane's existing set. + */ +export type AddRemotePoolParams = RemotePoolParams + +/** {@link AddRemotePoolParams} as {@link AddRemotePool.parse} leaves it. */ +type ParsedAddRemotePoolParams = ParsedRemotePoolParams + +/** Encodes `addRemotePool` calldata against the resolved pool {@link Interface}. */ +type Encoder = (iface: Interface, params: ParsedAddRemotePoolParams) => UnsignedEVMTx + +const encodeAddRemotePool: Encoder = ( + iface, + { poolAddress, remoteChainSelector, remotePoolAddress }, +) => + callTx( + poolAddress, + iface.encodeFunctionData('addRemotePool', [remoteChainSelector, remotePoolAddress]), + ) + +/** Authorizes an additional remote pool on one lane of a v1.5.1+ pool via `addRemotePool`. */ +export class AddRemotePool extends EVMOperation { + readonly name = 'addRemotePool' + + /** + * v1.5.1 and up, where the function was introduced and has not changed since — one entry + * covers v1.6.1 and v2.0.0 by {@link resolveEncoder}'s floor-match. No `null` ceiling is + * needed at the bottom: v1.5.0 matches nothing at or below itself and is reported unsupported + * for free. + */ + private readonly encoders: Partial> = { + [TokenPoolVersion.V1_5_1]: encodeAddRemotePool, + } + + /** + * Validates the pool address, lane selector and remote pool bytes before any RPC, keeping the + * parsed `remotePoolAddress` so {@link buildUnsigned} checks and encodes it without re-parsing. + */ + protected override parse(params: AddRemotePoolParams): ParsedAddRemotePoolParams { + return parseRemotePoolParams(this.name, params) + } + + /** + * Resolves the pool's version (rejecting v1.5.0), confirms `sender` owns the pool, then rejects + * a duplicate: the lane's currently registered remote pools are read scoped to this one + * selector, and an address already among them would revert on-chain + * (`PoolAlreadyAdded`). A lane that is not configured yet reads as having none — see + * {@link readRegisteredRemotePools}. + * @throws {@link CCTOperationUnsupportedError} if the pool is v1.5.0 + * @throws {@link CCTParamsInvalidError} if `sender` is given and is not the pool owner, or if + * `remotePoolAddress` is already registered on this lane + */ + protected async buildUnsigned( + chain: EVMChain, + params: ParsedAddRemotePoolParams, + ): Promise { + const { type, version } = await resolveTokenPool(chain, params.poolAddress) + // resolved before any further RPC, so an unsupported version fails on one call + const encode = resolveEncoder(this.encoders, version, this.name) + // owner-gated on-chain; surface it as a param error here instead of an on-chain revert + if (params.sender !== undefined) + await assertPoolOwner(this.name, chain, params.poolAddress, params.sender) + + const registered = await readRegisteredRemotePools(chain, params) + if (isRegisteredRemotePool(registered, params.remotePoolAddress, params.remoteChainSelector)) + throw new CCTParamsInvalidError( + this.name, + 'remotePoolAddress', + `is already registered on chain selector ${params.remoteChainSelector} (registered: ${registered.join(', ')}); adding it again reverts`, + ) + + chain.logger.debug( + `${this.name}: pool = ${params.poolAddress}, lane = ${params.remoteChainSelector}, registered = ${registered.length}`, + ) + return encode(getTokenPoolInterface(type, version), params) + } + + /** + * Signs and submits as the pool owner, defaulting `sender` to the signing wallet — the only + * address that can satisfy {@link buildUnsigned}'s owner check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} 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, or + * if any other param is invalid (see {@link buildUnsigned}) + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/remove-remote-pool.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/remove-remote-pool.test.ts new file mode 100644 index 000000000..e4d1b4b91 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/remove-remote-pool.test.ts @@ -0,0 +1,391 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError, toBeHex, zeroPadValue } from 'ethers' + +import type { TokenPoolRemote } from '../../../../chain.ts' +import { + CCIPExecTxRevertedError, + CCIPTokenPoolChainConfigNotFoundError, + CCIPWalletInvalidError, +} from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { parseTypeAndVersion } from '../../../../utils.ts' +import { CCTOperationUnsupportedError, CCTParamsInvalidError } from '../../../errors.ts' +import { + type TokenPoolType, + TOKEN_POOL_INTERFACES, + TokenPoolVersion, + getTokenPoolFamily, +} from '../contracts.ts' +import { type RemoveRemotePoolParams, RemoveRemotePool } from './remove-remote-pool.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 LOCKBOX = '0x' + '77'.repeat(20) +const NOT_THE_OWNER = '0x' + '88'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +/** The remote pool being removed — registered on the lane in the default stub. */ +const REMOTE_POOL = '0x' + '99'.repeat(20) +/** A remote pool that stays registered. */ +const OTHER_REMOTE_POOL = '0x' + 'aa'.repeat(20) + +const SELECTOR = 5009297550715157269n // ethereum-mainnet +const SOLANA_SELECTOR = 16423721717087811551n // solana-devnet + +/** Calldata built from a fresh Interface — never the SDK's cached one, or this proves nothing. */ +const FRESH = new Interface([ + 'function removeRemotePool(uint64 remoteChainSelector, bytes remotePoolAddress)', +]) +const expectedData = (remotePoolAddress = REMOTE_POOL, selector = SELECTOR) => + FRESH.encodeFunctionData('removeRemotePool', [selector, remotePoolAddress]) + +/** The `getTokenPoolState` getters the owner gate reads, per version generation. */ +function poolReads(version: TokenPoolVersion, type: TokenPoolType, owner: string) { + if (version !== TokenPoolVersion.V2_0_0) + return { + getToken: [TOKEN], + owner: [owner], + getRouter: [ROUTER], + getRmnProxy: [RMN_PROXY], + getRateLimitAdmin: [RATE_LIMIT_ADMIN], + getSupportedChains: [[SELECTOR]], + } + return { + getToken: [TOKEN], + owner: [owner], + getRmnProxy: [RMN_PROXY], + getTokenDecimals: [18], + getSupportedChains: [[SELECTOR]], + getDynamicConfig: [ROUTER, RATE_LIMIT_ADMIN, RATE_LIMIT_ADMIN], + getAllowedFinalityConfig: [toBeHex(0, 4)], + ...(getTokenPoolFamily(type) === 'LockRelease' ? { getLockBox: [LOCKBOX] } : {}), + } +} + +type Calls = { typeAndVersion: number; remotes: Array<[string, bigint | undefined]>; calls: number } + +/** + * EVMChain stub: reports `type`/`version`, answers the owner-gate getters off the pool's own + * Interface, and returns (or throws for) one lane's remotes. + */ +function stubChain({ + type = 'BurnMintTokenPool', + version = TokenPoolVersion.V1_5_1, + owner = OWNER, + remotePools = [REMOTE_POOL] as string[], + remotesError, + seen = { typeAndVersion: 0, remotes: [], calls: 0 }, +}: { + type?: TokenPoolType + version?: TokenPoolVersion + owner?: string + remotePools?: string[] + remotesError?: Error + seen?: Calls +} = {}): EVMChain { + const iface = TOKEN_POOL_INTERFACES[getTokenPoolFamily(type)][version] + const responses = new Map( + Object.entries(poolReads(version, type, owner)).map(([fn, values]) => [ + iface.getFunction(fn)!.selector, + iface.encodeFunctionResult(fn, values), + ]), + ) + const remote: TokenPoolRemote = { + remoteToken: TOKEN, + remotePools, + inboundRateLimiterState: null, + outboundRateLimiterState: null, + } + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + seen.calls++ + 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 Promise.resolve(encoded) + }, + }, + typeAndVersion: () => { + seen.typeAndVersion++ + return Promise.resolve(parseTypeAndVersion(`${type} ${version}`)) + }, + getTokenInfo: () => Promise.resolve({ decimals: 18, symbol: 'TKN', name: 'Token' }), + getTokenPoolRemotes: (tokenPool: string, remoteChainSelector?: bigint) => { + seen.remotes.push([tokenPool, remoteChainSelector]) + return remotesError ? Promise.reject(remotesError) : Promise.resolve({ 'a-network': remote }) + }, + nextNonce: () => Promise.resolve(0), + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(waitError?: Error, address = OWNER) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + 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 RemoveRemotePool() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddress: REMOTE_POOL, + sender: OWNER, + ...overrides, + }) +} + +/** Versions that declare `removeRemotePool`, each with both ABI families. */ +const SUPPORTED = [TokenPoolVersion.V1_5_1, TokenPoolVersion.V1_6_1, TokenPoolVersion.V2_0_0] +const TYPES: TokenPoolType[] = ['BurnMintTokenPool', 'LockReleaseTokenPool'] + +describe('RemoveRemotePool (cct/evm)', () => { + describe('generate', () => { + for (const version of SUPPORTED) { + for (const type of TYPES) { + it(`encodes removeRemotePool(selector, bytes) for a ${type} ${version}`, async () => { + const unsigned = await generate(stubChain({ type, version })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, POOL) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, expectedData()) + }) + } + } + + it('produces identical calldata for both ABI families', async () => { + const [burnMint, lockRelease] = await Promise.all( + TYPES.map(async (type) => (await generate(stubChain({ type }))).transactions[0]!.data), + ) + assert.equal(burnMint, lockRelease) + assert.equal(burnMint, expectedData()) + }) + + it('accepts a remotePoolAddress without the 0x prefix', async () => { + const unsigned = await generate(stubChain(), { + remotePoolAddress: REMOTE_POOL.slice(2).toUpperCase(), + }) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + }) + + it('encodes a 32-byte non-EVM remote pool address as-is', async () => { + const remotePoolAddress = '0x' + 'cd'.repeat(32) + const unsigned = await generate(stubChain({ remotePools: [remotePoolAddress] }), { + remoteChainSelector: SOLANA_SELECTOR, + remotePoolAddress, + }) + assert.equal(unsigned.transactions[0]!.data, expectedData(remotePoolAddress, SOLANA_SELECTOR)) + }) + + it('omits from, and skips the owner read, when sender is not supplied', async () => { + const seen: Calls = { typeAndVersion: 0, remotes: [], calls: 0 } + const unsigned = await generate(stubChain({ seen }), { sender: undefined }) + assert.equal(unsigned.transactions[0]!.from, undefined) + assert.equal(seen.calls, 0, 'no owner gate without a sender to compare') + }) + + it('scopes the remotes read to the one lane', async () => { + const seen: Calls = { typeAndVersion: 0, remotes: [], calls: 0 } + await generate(stubChain({ seen })) + assert.deepEqual(seen.remotes, [[POOL, SELECTOR]]) + }) + }) + + describe('validation', () => { + for (const [param, value] of [ + ['poolAddress', 'not-an-address'], + // a tx to `0x0` hits no code, so it mines as a successful no-op instead of reverting + ['poolAddress', ZeroAddress], + ['sender', 'not-an-address'], + ['remoteChainSelector', 1 as never], + ['remoteChainSelector', -1n], + ['remoteChainSelector', 2n ** 64n], + ['remotePoolAddress', ''], + ['remotePoolAddress', '0x'], + ['remotePoolAddress', '0xabc'], + ['remotePoolAddress', '0xzz'], + ['remotePoolAddress', 42 as never], + ] as const) { + it(`rejects ${param} = ${String(value)} before any RPC`, async () => { + const seen: Calls = { typeAndVersion: 0, remotes: [], calls: 0 } + await assert.rejects( + () => generate(stubChain({ seen }), { [param]: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'removeRemotePool' && + err.context.param === param, + ) + assert.deepEqual([seen.typeAndVersion, seen.remotes.length, seen.calls], [0, 0, 0]) + }) + } + }) + + describe('version dispatch', () => { + it('is unsupported on v1.5.0, which has no removal primitive', async () => { + const seen: Calls = { typeAndVersion: 0, remotes: [], calls: 0 } + await assert.rejects( + () => generate(stubChain({ version: TokenPoolVersion.V1_5_0, seen })), + (err: unknown) => + err instanceof CCTOperationUnsupportedError && + err.context.operation === 'removeRemotePool' && + err.context.version === '1.5.0', + ) + // the encoder is resolved off the single typeAndVersion read, before any further RPC + assert.deepEqual([seen.typeAndVersion, seen.remotes.length, seen.calls], [1, 0, 0]) + }) + + for (const version of SUPPORTED) { + it(`encodes on v${version}`, async () => { + const unsigned = await generate(stubChain({ version })) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + }) + } + + it('covers every known pool version', () => { + assert.deepEqual(Object.values(TokenPoolVersion), [TokenPoolVersion.V1_5_0, ...SUPPORTED]) + }) + }) + + describe('pre-transaction validation', () => { + it('rejects a remote pool that is not registered on the lane', async () => { + await assert.rejects( + () => generate(stubChain({ remotePools: [OTHER_REMOTE_POOL] })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'removeRemotePool' && + err.context.param === 'remotePoolAddress', + ) + }) + + it('matches a registered pool given as left-padded 32-byte bytes', async () => { + // the chain reader returns decoded 20-byte addresses; the caller may pass either form + const unsigned = await generate(stubChain({ remotePools: [REMOTE_POOL] }), { + remotePoolAddress: zeroPadValue(REMOTE_POOL, 32), + }) + assert.equal( + unsigned.transactions[0]!.data, + expectedData(zeroPadValue(REMOTE_POOL, 32).toLowerCase()), + ) + }) + + it('matches a registered pool whose spelling differs only in case', async () => { + const unsigned = await generate( + stubChain({ remotePools: [REMOTE_POOL.toUpperCase().replace('0X', '0x')] }), + ) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + }) + + it('falls back to raw byte comparison when the remote family has no registered codec', async () => { + // `decodeAddress` only knows the families whose chain module is loaded (EVM always is); + // for anything else the undecoded hex is compared + const bytes = '0x' + 'cd'.repeat(32) + const unsigned = await generate(stubChain({ remotePools: [bytes] }), { + remoteChainSelector: SOLANA_SELECTOR, + remotePoolAddress: bytes.toUpperCase().replace('0X', '0x'), + }) + assert.equal(unsigned.transactions[0]!.data, expectedData(bytes, SOLANA_SELECTOR)) + }) + + it('removes one of several registered pools', async () => { + const unsigned = await generate(stubChain({ remotePools: [OTHER_REMOTE_POOL, REMOTE_POOL] })) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + }) + + it('rejects removal on a lane that has no configuration at all', async () => { + const chain = stubChain({ + remotesError: new CCIPTokenPoolChainConfigNotFoundError(POOL, POOL, 'a-network'), + }) + await assert.rejects( + () => generate(chain), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'remotePoolAddress', + ) + }) + + it('propagates any other remotes-read failure', async () => { + const boom = new Error('rpc down') + await assert.rejects(() => generate(stubChain({ remotesError: boom })), boom) + }) + + it('rejects a sender that is not the pool owner', async () => { + await assert.rejects( + () => generate(stubChain({ owner: NOT_THE_OWNER })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'removeRemotePool' && + err.context.param === 'sender', + ) + }) + }) + + describe('execute', () => { + const params = { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddress: REMOTE_POOL, + } + + it('signs and submits as the owner, resolving to the tx hash', async () => { + assert.deepEqual(await op.execute(stubChain(), { ...params, wallet: fakeSigner() }), { + hash: HASH, + }) + }) + + it('maps an on-chain revert to CCIPExecTxRevertedError', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + ...params, + wallet: fakeSigner(makeError('execution reverted', 'CALL_EXCEPTION')), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && err.context.operation === 'removeRemotePool', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, wallet: {} }), + CCIPWalletInvalidError, + ) + }) + + it('rejects a sender that is not the signing wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, sender: NOT_THE_OWNER, wallet: fakeSigner() }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'removeRemotePool' && + err.context.param === 'sender', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/remove-remote-pool.ts b/ccip-sdk/src/cct/evm/token-pool/operations/remove-remote-pool.ts new file mode 100644 index 000000000..4b943a2eb --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/remove-remote-pool.ts @@ -0,0 +1,131 @@ +/** + * removeRemotePool: de-authorizes one remote pool address on a lane (v1.5.1+). + * + * @remarks **v1.5.1 and newer**, the versions where a lane holds a *set* of remote pools. The + * counterpart to {@link AddRemotePool}, and the last step of a remote-side pool upgrade: add the + * new pool, drain the old, then remove it. v1.5.0 has no removal primitive — its single remote + * pool can only be overwritten via {@link SetRemotePool} — so this op reports itself unsupported + * there rather than emulating a removal. + * + * @packageDocumentation + */ + +import type { Interface } 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 { + TokenPoolVersion, + assertPoolOwner, + getTokenPoolInterface, + resolveEncoder, + resolveTokenPool, +} from '../contracts.ts' +import { + type ParsedRemotePoolParams, + type RemotePoolParams, + isRegisteredRemotePool, + parseRemotePoolParams, + readRegisteredRemotePools, +} from '../remote-pool.ts' + +/** + * Parameters for {@link RemoveRemotePool} — see {@link RemotePoolParams}; `remotePoolAddress` is + * the remote chain's pool address as hex bytes, removed from the lane's existing set. + */ +export type RemoveRemotePoolParams = RemotePoolParams + +/** {@link RemoveRemotePoolParams} as {@link RemoveRemotePool.parse} leaves it. */ +type ParsedRemoveRemotePoolParams = ParsedRemotePoolParams + +/** Encodes `removeRemotePool` calldata against the resolved pool {@link Interface}. */ +type Encoder = (iface: Interface, params: ParsedRemoveRemotePoolParams) => UnsignedEVMTx + +const encodeRemoveRemotePool: Encoder = ( + iface, + { poolAddress, remoteChainSelector, remotePoolAddress }, +) => + callTx( + poolAddress, + iface.encodeFunctionData('removeRemotePool', [remoteChainSelector, remotePoolAddress]), + ) + +/** De-authorizes a remote pool on one lane of a v1.5.1+ pool via `removeRemotePool`. */ +export class RemoveRemotePool extends EVMOperation< + RemoveRemotePoolParams, + ParsedRemoveRemotePoolParams +> { + readonly name = 'removeRemotePool' + + /** + * v1.5.1 and up, where the function was introduced and has not changed since — one entry + * covers v1.6.1 and v2.0.0 by {@link resolveEncoder}'s floor-match. No `null` ceiling is + * needed at the bottom: v1.5.0 matches nothing at or below itself and is reported unsupported + * for free. + */ + private readonly encoders: Partial> = { + [TokenPoolVersion.V1_5_1]: encodeRemoveRemotePool, + } + + /** + * Validates the pool address, lane selector and remote pool bytes before any RPC, keeping the + * parsed `remotePoolAddress` so {@link buildUnsigned} checks and encodes it without re-parsing. + */ + protected override parse(params: RemoveRemotePoolParams): ParsedRemoveRemotePoolParams { + return parseRemotePoolParams(this.name, params) + } + + /** + * Resolves the pool's version (rejecting v1.5.0), confirms `sender` owns the pool, then requires + * the address to actually be registered on this lane: the lane's remote pools are read scoped to + * this one selector, and removing an address that is not among them would revert on-chain + * (`InvalidRemotePoolForChain`). An unconfigured lane reads as having none — see + * {@link readRegisteredRemotePools} — and is rejected the same way. + * @throws {@link CCTOperationUnsupportedError} if the pool is v1.5.0 + * @throws {@link CCTParamsInvalidError} if `sender` is given and is not the pool owner, or if + * `remotePoolAddress` is not currently registered on this lane + */ + protected async buildUnsigned( + chain: EVMChain, + params: ParsedRemoveRemotePoolParams, + ): Promise { + const { type, version } = await resolveTokenPool(chain, params.poolAddress) + // resolved before any further RPC, so an unsupported version fails on one call + const encode = resolveEncoder(this.encoders, version, this.name) + // owner-gated on-chain; surface it as a param error here instead of an on-chain revert + if (params.sender !== undefined) + await assertPoolOwner(this.name, chain, params.poolAddress, params.sender) + + const registered = await readRegisteredRemotePools(chain, params) + if (!isRegisteredRemotePool(registered, params.remotePoolAddress, params.remoteChainSelector)) + throw new CCTParamsInvalidError( + this.name, + 'remotePoolAddress', + `is not registered on chain selector ${params.remoteChainSelector} (registered: ${registered.join(', ') || 'none'}); removing it reverts`, + ) + + chain.logger.debug( + `${this.name}: pool = ${params.poolAddress}, lane = ${params.remoteChainSelector}, registered = ${registered.length}`, + ) + return encode(getTokenPoolInterface(type, version), params) + } + + /** + * Signs and submits as the pool owner, defaulting `sender` to the signing wallet — the only + * address that can satisfy {@link buildUnsigned}'s owner check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} 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, or + * if any other param is invalid (see {@link buildUnsigned}) + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/set-remote-pool.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/set-remote-pool.test.ts new file mode 100644 index 000000000..55f18e1d9 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/set-remote-pool.test.ts @@ -0,0 +1,326 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError, toBeHex } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { parseTypeAndVersion } from '../../../../utils.ts' +import { CCTOperationUnsupportedError, CCTParamsInvalidError } from '../../../errors.ts' +import { + type TokenPoolType, + TOKEN_POOL_INTERFACES, + TokenPoolVersion, + getTokenPoolFamily, +} from '../contracts.ts' +import { type SetRemotePoolParams, SetRemotePool } from './set-remote-pool.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 LOCKBOX = '0x' + '77'.repeat(20) +const NOT_THE_OWNER = '0x' + '88'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +/** The remote pool this lane is being pointed at. */ +const REMOTE_POOL = '0x' + '99'.repeat(20) + +const SELECTOR = 5009297550715157269n // ethereum-mainnet +const SOLANA_SELECTOR = 16423721717087811551n // solana-devnet + +/** Calldata built from a fresh Interface — never the SDK's cached one, or this proves nothing. */ +const FRESH = new Interface([ + 'function setRemotePool(uint64 remoteChainSelector, bytes remotePoolAddress)', +]) +const expectedData = (remotePoolAddress = REMOTE_POOL, selector = SELECTOR) => + FRESH.encodeFunctionData('setRemotePool', [selector, remotePoolAddress]) + +/** The `getTokenPoolState` getters the owner gate reads, per version generation. */ +function poolReads(version: TokenPoolVersion, type: TokenPoolType, owner: string) { + if (version !== TokenPoolVersion.V2_0_0) + return { + getToken: [TOKEN], + owner: [owner], + getRouter: [ROUTER], + getRmnProxy: [RMN_PROXY], + getRateLimitAdmin: [RATE_LIMIT_ADMIN], + getSupportedChains: [[SELECTOR]], + } + return { + getToken: [TOKEN], + owner: [owner], + getRmnProxy: [RMN_PROXY], + getTokenDecimals: [18], + getSupportedChains: [[SELECTOR]], + getDynamicConfig: [ROUTER, RATE_LIMIT_ADMIN, RATE_LIMIT_ADMIN], + getAllowedFinalityConfig: [toBeHex(0, 4)], + ...(getTokenPoolFamily(type) === 'LockRelease' ? { getLockBox: [LOCKBOX] } : {}), + } +} + +type Calls = { typeAndVersion: number; remotes: number; calls: number } + +/** + * EVMChain stub: reports `type`/`version` and answers the owner-gate getters off the pool's own + * Interface. `getTokenPoolRemotes` is wired only to prove this op never calls it — a wholesale + * replace has no membership precondition to check. + */ +function stubChain({ + type = 'BurnMintTokenPool', + version = TokenPoolVersion.V1_5_0, + owner = OWNER, + seen = { typeAndVersion: 0, remotes: 0, calls: 0 }, +}: { + type?: TokenPoolType + version?: TokenPoolVersion + owner?: string + seen?: Calls +} = {}): EVMChain { + const iface = TOKEN_POOL_INTERFACES[getTokenPoolFamily(type)][version] + const responses = new Map( + Object.entries(poolReads(version, type, owner)).map(([fn, values]) => [ + iface.getFunction(fn)!.selector, + iface.encodeFunctionResult(fn, values), + ]), + ) + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + seen.calls++ + 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 Promise.resolve(encoded) + }, + }, + typeAndVersion: () => { + seen.typeAndVersion++ + return Promise.resolve(parseTypeAndVersion(`${type} ${version}`)) + }, + getTokenInfo: () => Promise.resolve({ decimals: 18, symbol: 'TKN', name: 'Token' }), + getTokenPoolRemotes: () => { + seen.remotes++ + return Promise.resolve({}) + }, + nextNonce: () => Promise.resolve(0), + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(waitError?: Error, address = OWNER) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + 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 SetRemotePool() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddress: REMOTE_POOL, + sender: OWNER, + ...overrides, + }) +} + +/** The versions that dropped `setRemotePool` from the ABI. */ +const UNSUPPORTED = [TokenPoolVersion.V1_5_1, TokenPoolVersion.V1_6_1, TokenPoolVersion.V2_0_0] +const TYPES: TokenPoolType[] = ['BurnMintTokenPool', 'LockReleaseTokenPool'] + +describe('SetRemotePool (cct/evm)', () => { + describe('generate', () => { + for (const type of TYPES) { + it(`encodes setRemotePool(selector, bytes) for a ${type} 1.5.0`, async () => { + const unsigned = await generate(stubChain({ type })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, POOL) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, expectedData()) + }) + } + + it('produces identical calldata for both ABI families', async () => { + const [burnMint, lockRelease] = await Promise.all( + TYPES.map(async (type) => (await generate(stubChain({ type }))).transactions[0]!.data), + ) + assert.equal(burnMint, lockRelease) + assert.equal(burnMint, expectedData()) + }) + + it('accepts a remotePoolAddress without the 0x prefix', async () => { + const unsigned = await generate(stubChain(), { + remotePoolAddress: REMOTE_POOL.slice(2).toUpperCase(), + }) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + }) + + it('encodes a 32-byte non-EVM remote pool address as-is', async () => { + const remotePoolAddress = '0x' + 'cd'.repeat(32) + const unsigned = await generate(stubChain(), { + remoteChainSelector: SOLANA_SELECTOR, + remotePoolAddress, + }) + assert.equal(unsigned.transactions[0]!.data, expectedData(remotePoolAddress, SOLANA_SELECTOR)) + }) + + it('omits from, and skips the owner read, when sender is not supplied', async () => { + const seen: Calls = { typeAndVersion: 0, remotes: 0, calls: 0 } + const unsigned = await generate(stubChain({ seen }), { sender: undefined }) + assert.equal(unsigned.transactions[0]!.from, undefined) + assert.equal(seen.calls, 0, 'no owner gate without a sender to compare') + }) + + it('never reads the lane: a wholesale replace has no membership precondition', async () => { + const seen: Calls = { typeAndVersion: 0, remotes: 0, calls: 0 } + await generate(stubChain({ seen })) + assert.equal(seen.remotes, 0) + }) + }) + + describe('validation', () => { + for (const [param, value] of [ + ['poolAddress', 'not-an-address'], + // a tx to `0x0` hits no code, so it mines as a successful no-op instead of reverting + ['poolAddress', ZeroAddress], + ['sender', 'not-an-address'], + ['remoteChainSelector', 1 as never], + ['remoteChainSelector', -1n], + ['remoteChainSelector', 2n ** 64n], + ['remotePoolAddress', ''], + ['remotePoolAddress', '0x'], + ['remotePoolAddress', '0xabc'], + ['remotePoolAddress', '0xzz'], + ['remotePoolAddress', 42 as never], + ] as const) { + it(`rejects ${param} = ${String(value)} before any RPC`, async () => { + const seen: Calls = { typeAndVersion: 0, remotes: 0, calls: 0 } + await assert.rejects( + () => generate(stubChain({ seen }), { [param]: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setRemotePool' && + err.context.param === param, + ) + assert.deepEqual([seen.typeAndVersion, seen.remotes, seen.calls], [0, 0, 0]) + }) + } + }) + + describe('version dispatch', () => { + it('encodes on v1.5.0, the only version that declares setRemotePool', async () => { + const unsigned = await generate(stubChain({ version: TokenPoolVersion.V1_5_0 })) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + }) + + for (const version of UNSUPPORTED) { + it(`is unsupported on v${version}, which dropped the function`, async () => { + // the null ceiling at v1.5.1 is what stops the floor-match from inheriting the v1.5.0 + // encoder here and emitting calldata for a selector these pools do not implement + const seen: Calls = { typeAndVersion: 0, remotes: 0, calls: 0 } + await assert.rejects( + () => generate(stubChain({ version, seen })), + (err: unknown) => + err instanceof CCTOperationUnsupportedError && + err.context.operation === 'setRemotePool' && + err.context.version === version, + ) + assert.deepEqual([seen.typeAndVersion, seen.remotes, seen.calls], [1, 0, 0]) + }) + } + + it('covers every known pool version', () => { + assert.deepEqual(Object.values(TokenPoolVersion), [TokenPoolVersion.V1_5_0, ...UNSUPPORTED]) + }) + + it('has no setRemotePool in any post-1.5.0 vendored ABI', () => { + for (const version of UNSUPPORTED) { + for (const family of ['BurnMint', 'LockRelease'] as const) { + assert.equal( + TOKEN_POOL_INTERFACES[family][version].getFunction('setRemotePool'), + null, + `${family} ${version}`, + ) + } + } + }) + }) + + describe('pre-transaction validation', () => { + it('rejects a sender that is not the pool owner', async () => { + await assert.rejects( + () => generate(stubChain({ owner: NOT_THE_OWNER })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setRemotePool' && + err.context.param === 'sender', + ) + }) + }) + + describe('execute', () => { + const params = { + poolAddress: POOL, + remoteChainSelector: SELECTOR, + remotePoolAddress: REMOTE_POOL, + } + + it('signs and submits as the owner, resolving to the tx hash', async () => { + assert.deepEqual(await op.execute(stubChain(), { ...params, wallet: fakeSigner() }), { + hash: HASH, + }) + }) + + it('maps an on-chain revert to CCIPExecTxRevertedError', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + ...params, + wallet: fakeSigner(makeError('execution reverted', 'CALL_EXCEPTION')), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && err.context.operation === 'setRemotePool', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, wallet: {} }), + CCIPWalletInvalidError, + ) + }) + + it('rejects a sender that is not the signing wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, sender: NOT_THE_OWNER, wallet: fakeSigner() }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setRemotePool' && + err.context.param === 'sender', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/set-remote-pool.ts b/ccip-sdk/src/cct/evm/token-pool/operations/set-remote-pool.ts new file mode 100644 index 000000000..1c294f0bb --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/set-remote-pool.ts @@ -0,0 +1,112 @@ +/** + * setRemotePool: replaces the remote pool address a v1.5.0 pool accepts on one lane. + * + * @remarks **v1.5.0 only.** A 1.5.0 pool holds exactly one remote pool per lane and this call + * overwrites it wholesale; v1.5.1 replaced it with the additive `addRemotePool` / + * `removeRemotePool` pair (a lane may hold several remote pools there), and no version from + * v1.5.1 up declares `setRemotePool` at all. Emulating it on a newer pool is deliberately not + * attempted — "replace" over a set of unknown size is not a single transaction — so this op + * reports itself unsupported there instead of guessing. + * + * @packageDocumentation + */ + +import type { Interface } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import type { TransactionResult } from '../../../operation.ts' +import { type EVMExecuteParams, EVMOperation, callTx } from '../../operation.ts' +import { + TokenPoolVersion, + assertPoolOwner, + getTokenPoolInterface, + resolveEncoder, + resolveTokenPool, +} from '../contracts.ts' +import { + type ParsedRemotePoolParams, + type RemotePoolParams, + parseRemotePoolParams, +} from '../remote-pool.ts' + +/** + * Parameters for {@link SetRemotePool} — see {@link RemotePoolParams}; `remotePoolAddress` is the + * remote chain's pool address as hex bytes, which becomes the lane's *only* remote pool. + */ +export type SetRemotePoolParams = RemotePoolParams + +/** {@link SetRemotePoolParams} as {@link SetRemotePool.parse} leaves it. */ +type ParsedSetRemotePoolParams = ParsedRemotePoolParams + +/** Encodes `setRemotePool` calldata against the resolved pool {@link Interface}. */ +type Encoder = (iface: Interface, params: ParsedSetRemotePoolParams) => UnsignedEVMTx + +const encodeSetRemotePool: Encoder = ( + iface, + { poolAddress, remoteChainSelector, remotePoolAddress }, +) => + callTx( + poolAddress, + iface.encodeFunctionData('setRemotePool', [remoteChainSelector, remotePoolAddress]), + ) + +/** Replaces a v1.5.0 pool's remote pool for one lane via `setRemotePool`. */ +export class SetRemotePool extends EVMOperation { + readonly name = 'setRemotePool' + + /** + * v1.5.0 only. The explicit `null` at v1.5.1 is load-bearing: it is the removal ceiling + * {@link resolveEncoder} stops its floor-match walk at, so v1.5.1/v1.6.1/v2.0.0 report the op + * as unsupported. Without it they would inherit the v1.5.0 encoder and emit calldata for a + * function selector those pools do not implement. + */ + private readonly encoders: Partial> = { + [TokenPoolVersion.V1_5_0]: encodeSetRemotePool, + [TokenPoolVersion.V1_5_1]: null, + } + + /** + * Validates the pool address, lane selector and remote pool bytes before any RPC, keeping the + * parsed `remotePoolAddress` so {@link buildUnsigned} encodes it without re-parsing. + */ + protected override parse(params: SetRemotePoolParams): ParsedSetRemotePoolParams { + return parseRemotePoolParams(this.name, params) + } + + /** + * Resolves the pool's version (rejecting anything past v1.5.0), confirms `sender` owns the + * pool, then encodes the call. No membership precondition: this call replaces whatever the lane + * held, so there is nothing to check it against. + * @throws {@link CCTOperationUnsupportedError} if the pool is v1.5.1 or newer + * @throws {@link CCTParamsInvalidError} if `sender` is given and is not the pool owner + */ + protected async buildUnsigned( + chain: EVMChain, + params: ParsedSetRemotePoolParams, + ): Promise { + const { type, version } = await resolveTokenPool(chain, params.poolAddress) + // resolved before any further RPC, so an unsupported version fails on one call + const encode = resolveEncoder(this.encoders, version, this.name) + // owner-gated on-chain; surface it as a param error here instead of an on-chain revert + if (params.sender !== undefined) + await assertPoolOwner(this.name, chain, params.poolAddress, params.sender) + return encode(getTokenPoolInterface(type, version), params) + } + + /** + * Signs and submits as the pool owner, defaulting `sender` to the signing wallet — the only + * address that can satisfy {@link buildUnsigned}'s owner check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} 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, or + * if any other param is invalid (see {@link buildUnsigned}) + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} 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 index cbaf07dbd..fbd0350b0 100644 --- a/ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts +++ b/ccip-sdk/src/cct/evm/token-pool/operations/transfer-ownership.ts @@ -10,7 +10,7 @@ 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 { validateAddress, validateNonZeroAddress } from '../../validate.ts' import { TokenPoolVersion, getTokenPoolInterface, @@ -46,7 +46,7 @@ export class TransferOwnership extends EVMOperation { /** Validates the pool and new-owner addresses before any RPC. */ protected override validate({ poolAddress, newOwner }: TransferOwnershipParams): void { - validateAddress(this.name, 'poolAddress', poolAddress) + validateNonZeroAddress(this.name, 'poolAddress', poolAddress) validateAddress(this.name, 'newOwner', newOwner) } diff --git a/ccip-sdk/src/cct/evm/token-pool/remote-pool.ts b/ccip-sdk/src/cct/evm/token-pool/remote-pool.ts new file mode 100644 index 000000000..b5767d193 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/remote-pool.ts @@ -0,0 +1,145 @@ +/** + * Shared internals of the three remote-pool write ops — `setRemotePool` (v1.5.0), + * `addRemotePool` and `removeRemotePool` (v1.5.1+): the parameter shape they have in common, + * `remotePoolAddress` parsing, and the per-lane membership read the add/remove preconditions + * are checked against. The owner gate itself is `assertPoolOwner` in `../contracts.ts`, + * shared with every other owner-gated pool write. + * + * @packageDocumentation + */ + +import { isHexString } from 'ethers' + +import { CCIPTokenPoolChainConfigNotFoundError } from '../../../errors/index.ts' +import type { EVMChain } from '../../../evm/index.ts' +import { networkInfo } from '../../../networks.ts' +import { decodeAddress } from '../../../utils.ts' +import { parseHexBytes, validateNonZeroAddress, validateUint64 } from '../validate.ts' + +/** + * Parameters shared by every remote-pool write op: which pool, which lane, and which remote pool. + * + * @remarks `remotePoolAddress` is the *remote* chain's pool address as raw bytes, not an EVM + * address: the lane's other end may be Solana, Aptos or Sui, whose addresses are 32 bytes. The + * contracts take it as `bytes` for exactly that reason, so it is accepted here as hex of any + * (even-digit) length rather than validated as an EVM address. + */ +export type RemotePoolParams = { + /** Local token pool contract being reconfigured. */ + poolAddress: string + /** CCIP selector of the lane's remote chain (`uint64`). */ + remoteChainSelector: bigint + /** + * Remote chain's pool address as hex bytes; the `0x` prefix is optional. Any even number of + * hex digits is accepted — a non-EVM remote's address is not 20 bytes. + */ + remotePoolAddress: string + /** Current pool owner; sets `tx.from` for offline / multisig signing. */ + sender?: string +} + +/** + * {@link RemotePoolParams} as {@link parseRemotePoolParams} leaves it: `remotePoolAddress` + * normalised to 0x-prefixed lowercase hex, so `buildUnsigned` encodes it without re-parsing. + */ +export type ParsedRemotePoolParams = RemotePoolParams & { remotePoolAddress: string } + +/** + * Normalises `remotePoolAddress` to 0x-prefixed lowercase hex, the form `bytes` calldata is + * encoded from. + * @remarks Deliberately not an address check: see {@link RemotePoolParams.remotePoolAddress}. + * Only the encoding is constrained — hex digits, `0x` optional, whole bytes, non-empty. A thin + * alias over the shared {@link parseHexBytes} that fixes the param path these three ops all + * blame; the parser itself lives in `../validate.ts` alongside its sibling validators, shared + * with `applyChainUpdates`, which validates the same kind of value. + * @throws {@link CCTParamsInvalidError} if `value` is not a non-empty, whole-byte hex string + */ +export function parseRemotePoolAddress(operation: string, value: unknown): string { + return parseHexBytes(operation, 'remotePoolAddress', value) +} + +/** + * Validates the params every remote-pool op takes, before any RPC. + * @remarks `poolAddress` is required to be **non-zero**, not merely well formed: a call to `0x0` + * hits no code, so it would mine as a *successful* no-op rather than failing. + * @returns The parsed `remotePoolAddress` (0x-prefixed lowercase hex). + * @throws {@link CCTParamsInvalidError} if `poolAddress` is not a valid non-zero address, + * `remoteChainSelector` is not a `uint64`, or `remotePoolAddress` is not hex bytes + */ +export function validateRemotePoolParams(operation: string, params: RemotePoolParams): string { + validateNonZeroAddress(operation, 'poolAddress', params.poolAddress) + validateUint64(operation, 'remoteChainSelector', params.remoteChainSelector) + return parseRemotePoolAddress(operation, params.remotePoolAddress) +} + +/** + * The three ops' {@link Operation.parse}: validates every field before any RPC and returns the + * params with `remotePoolAddress` already normalised, so `buildUnsigned` encodes it without + * re-parsing. Spreads the result of {@link validateRemotePoolParams} back over the params. + * @throws {@link CCTParamsInvalidError} if any field is invalid (see {@link validateRemotePoolParams}) + */ +export function parseRemotePoolParams( + operation: string, + params: RemotePoolParams, +): ParsedRemotePoolParams { + return { ...params, remotePoolAddress: validateRemotePoolParams(operation, params) } +} + +/** + * Reads the remote pool addresses currently registered on one lane, as the pool reports them. + * + * @remarks Scoped to the single `remoteChainSelector` rather than scanning every supported + * chain — one `getRemotePools` call instead of one per lane. + * + * A lane with no configuration at all surfaces from + * {@link EVMChain.getTokenPoolRemotes} as {@link CCIPTokenPoolChainConfigNotFoundError} (it + * requires a non-zero remote token), not as an empty result. That is treated here as "no remote + * pools registered", which is what it means: an unconfigured lane cannot have any. + */ +export async function readRegisteredRemotePools( + chain: EVMChain, + { poolAddress, remoteChainSelector }: RemotePoolParams, +): Promise { + let remotes + try { + remotes = await chain.getTokenPoolRemotes(poolAddress, remoteChainSelector) + } catch (err) { + if (err instanceof CCIPTokenPoolChainConfigNotFoundError) return [] + throw err + } + // one selector in, at most one lane out — keyed by the remote network's name + return Object.values(remotes).flatMap(({ remotePools }) => remotePools) +} + +/** + * Whether `remotePoolAddress` (hex bytes) is among the lane's `registered` pools. + * + * @remarks The two sides arrive in different spellings: `registered` comes back from + * {@link EVMChain.getTokenPoolRemotes} already decoded into the *remote* family's address format + * (checksummed hex for EVM, base58 for Solana, …), while the caller passes raw `bytes`. So the + * caller's value is decoded through the same codec, with the remote chain's family taken from + * its selector, and only then compared. + * + * Comparison is exact, or case-insensitive when both sides are hex — which covers EVM checksum + * spellings without risking a false match between two base58 addresses that differ only in case. + * If the family is unknown or the bytes are not decodable as one of its addresses (e.g. an + * oddly sized value), the undecoded hex is compared instead, so an unrecognised lane degrades + * to a plain byte comparison rather than throwing. + */ +export function isRegisteredRemotePool( + registered: readonly string[], + remotePoolAddress: string, + remoteChainSelector: bigint, +): boolean { + let expected + try { + expected = decodeAddress(remotePoolAddress, networkInfo(remoteChainSelector).family) + } catch { + expected = remotePoolAddress + } + return registered.some( + (pool) => + pool === expected || + (isHexString(pool) && isHexString(expected) && pool.toLowerCase() === expected.toLowerCase()), + ) +} From 9b2fb0405e8b20e5e66edaaea5eb30762e72604e Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:44:49 +0100 Subject: [PATCH 76/87] feat(cct-sdk): Add set chain rate limiter configs op (#393) * feat(cct-sdk): Add get token pool remotes evm query * feat(cct-sdk): Add generic EVM param validators * feat(cct-sdk): Read siloed lock/release pool state * feat(cct-sdk): Add pool owner pre-flight and encoder removal ceilings * refactor(cct-sdk): Hoist validate/parse lifecycle to the shared Operation base * feat(cct-sdk): Add apply chain updates evm op * fix(cct-sdk): Reject a declared version that is not the pool's calldata shape * feat(cct-sdk): Add EVM remote pool ops * Address PR comments * feat(cct-sdk): Add set chain rate limiter configs op * Fix lint * Fix lint * Fix lint * Fix tsdoc and lint * cleanup --- ccip-sdk/src/cct/evm/index.ts | 151 ++++- ccip-sdk/src/cct/evm/token-pool/contracts.ts | 105 ++- .../operations/apply-chain-updates.ts | 213 +++--- .../set-chain-rate-limiter-configs.test.ts | 635 ++++++++++++++++++ .../set-chain-rate-limiter-configs.ts | 352 ++++++++++ .../src/cct/evm/token-pool/rate-limit.test.ts | 240 +++++++ ccip-sdk/src/cct/evm/token-pool/rate-limit.ts | 162 +++++ ccip-sdk/src/cct/evm/validate.ts | 45 +- 8 files changed, 1733 insertions(+), 170 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/rate-limit.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/rate-limit.ts diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 735a420c2..ca6f54899 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -48,10 +48,6 @@ import { type ApplyChainUpdatesParams, ApplyChainUpdates, } from './token-pool/operations/apply-chain-updates.ts' -import { - type ApplyChainUpdatesParams, - ApplyChainUpdates, -} from './token-pool/operations/apply-chain-updates.ts' import { type DeployTokenPoolParams, DeployTokenPool, @@ -70,6 +66,10 @@ import { type RemoveRemotePoolParams, RemoveRemotePool, } from './token-pool/operations/remove-remote-pool.ts' +import { + type SetChainRateLimiterConfigsParams, + SetChainRateLimiterConfigs, +} from './token-pool/operations/set-chain-rate-limiter-configs.ts' import { type SetRemotePoolParams, SetRemotePool } from './token-pool/operations/set-remote-pool.ts' import { type TransferOwnershipParams, @@ -100,6 +100,7 @@ export class EVMTokenManager extends TokenManager { readonly #addRemotePool = new AddRemotePool() readonly #removeRemotePool = new RemoveRemotePool() readonly #applyChainUpdates = new ApplyChainUpdates() + readonly #setChainRateLimiterConfigs = new SetChainRateLimiterConfigs() // Lockbox operations readonly #deployLockbox = new DeployLockbox() @@ -389,6 +390,102 @@ export class EVMTokenManager extends TokenManager { return this.#transferOwnership.execute(this.chain, opts) } + /** + * Builds an unsigned pool rate-limit tx (for multisig / offline signing): sets the inbound and + * outbound limits of one or more already-configured lanes, in a single transaction. Probes the + * pool's on-chain `typeAndVersion` to resolve its interface + encoder. + * @remarks **v1.5.0 pools set one lane per transaction.** v1.5.1/v1.6.1 encode the batch + * `setChainRateLimiterConfigs(uint64[], Config[], Config[])` and v2.0.0 the reshaped + * `setRateLimitConfig(RateLimitConfigArgs[])`, but v1.5.0 ships only the singular + * `setChainRateLimiterConfig(uint64, Config, Config)`. To keep the one-op-one-transaction + * contract every CCT write holds, a v1.5.0 pool therefore accepts only a single-element + * `updates`; a multi-lane batch is rejected with {@link CCTParamsInvalidError} rather than + * fanned out into N transactions. + * + * `fastFinality` is **v2.0.0-only** — the flag does not exist in the earlier ABIs, so setting it + * (to either value) on an older pool is rejected rather than silently dropped. It defaults to + * `false` on v2.0.0. + * + * This op *updates* limits on lanes that already exist; it does not add one. An unconfigured + * selector reverts on-chain (`NonExistentChain`). + * + * The tx must ultimately be signed by the pool `owner` **or** its `rateLimitAdmin` — both are + * reported by {@link getTokenPoolState}. When `opts.sender` is supplied it is pre-flighted + * against *both* roles (two extra `eth_call`s — the pool's `owner()` and whichever getter + * reports `rateLimitAdmin` on that version), so a + * `sender` holding neither fails at build time rather than reverting at signing. Omit `sender` + * to build the calldata without any role read, when the eventual signer is not yet known. + * @throws {@link CCTParamsInvalidError} if any param is invalid: `updates` empty, a repeated + * `remoteChainSelector`, a non-`uint64` selector, a rate above its capacity while enabled, a + * non-zero amount while disabled, `fastFinality` set on a pre-2.0.0 pool, or `sender` given and + * being neither the pool `owner` nor its (set) `rateLimitAdmin`. On a **v1.5.1** pool the + * enabled-bucket bound is stricter still (`0 < rate < capacity`), so a `rate` of `0n` or a + * `rate` equal to `capacity` is also rejected there — v1.6.1 and v2.0.0 allow both. A + * **v1.5.0** pool accepts only a single-element `updates`. + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedSetChainRateLimiterConfigs({ + * poolAddress: '0xPool...', + * updates: [ + * { + * remoteChainSelector: 5009297550715157269n, // ethereum-mainnet + * // amounts are in the local token's smallest unit (18 decimals here) + * outboundRateLimiterConfig: { enabled: true, capacity: 10_000n * 10n ** 18n, rate: 100n * 10n ** 18n }, + * inboundRateLimiterConfig: { enabled: false }, // capacity/rate default to 0n + * }, + * ], + * sender: '0xOwnerOrRateLimitAdmin...', + * }) + * ``` + */ + generateUnsignedSetChainRateLimiterConfigs( + opts: SetChainRateLimiterConfigsParams, + ): Promise { + return this.#setChainRateLimiterConfigs.generate(this.chain, opts) + } + + /** + * Sets the inbound and outbound rate limits of one or more already-configured lanes in a single + * transaction, signing + submitting with `opts.wallet`. + * @remarks Gated on **either** the pool `owner` or its `rateLimitAdmin` — rate limits are the one + * pool write that accepts a delegated role, so this check is a disjunction where + * {@link transferOwnership}'s is owner-only. Both roles are reported by + * {@link getTokenPoolState}; `rateLimitAdmin` is the zero address when unset, and an unset role + * matches nobody. + * + * Same version rules as {@link generateUnsignedSetChainRateLimiterConfigs}: **v1.5.0 pools set + * one lane per transaction**, and `fastFinality` is v2.0.0-only. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid, or if `sender` is given and is + * not the wallet's address, or the signer is neither the pool `owner` nor its (set) + * `rateLimitAdmin`. On a **v1.5.1** pool an enabled rate limiter must additionally satisfy + * `0 < rate < capacity`, so a `rate` of `0n` or a `rate` equal to `capacity` is rejected there — + * v1.6.1 and v2.0.0 allow both. A **v1.5.0** pool accepts only a single-element `updates`. + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.setChainRateLimiterConfigs({ + * poolAddress: '0xPool...', + * updates: [ + * { + * remoteChainSelector: 16015286601757825753n, // ethereum-testnet-sepolia + * outboundRateLimiterConfig: { enabled: true, capacity: 1_000n * 10n ** 18n, rate: 10n * 10n ** 18n }, + * inboundRateLimiterConfig: { enabled: true, capacity: 1_000n * 10n ** 18n, rate: 10n * 10n ** 18n }, + * // fastFinality: true, // v2.0.0 pools only — targets the fast-finality buckets + * }, + * ], + * wallet, // the pool owner or its rateLimitAdmin + * }) + * ``` + */ + setChainRateLimiterConfigs( + opts: EVMExecuteParams, + ): Promise { + return this.#setChainRateLimiterConfigs.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 — @@ -622,21 +719,39 @@ export class EVMTokenManager extends TokenManager { * chain, the `remoteToken`, the `remotePools` authorized to mint/release against it, and the * inbound/outbound rate-limiter buckets. Keyed by remote network name. * @remarks Omit `remoteChainSelector` to scan every lane the pool reports through - * `getSupportedChains()`; pass one to read a single lane. `inboundRateLimiterState` / - * `outboundRateLimiterState` are `null` when that direction is unlimited, and their amounts are - * in the *local* token's smallest unit; v2.0.0 pools add `fast*RateLimiterState` for - * Faster-Than-Finality and safe-finality (FCR) transfers. + * `getSupportedChains()`; provide it to read one, which is the cheaper call by far on a pool with + * many lanes. Passing a selector the pool has no config for surfaces as + * {@link CCIPTokenPoolChainConfigNotFoundError} rather than an empty result. + * + * A lane's rate limiter is nullable: `inboundRateLimiterState` / `outboundRateLimiterState` are + * `null` when that direction is unlimited, so check for `null` before reading `.capacity`. + * Amounts are in the *local* token's smallest unit. On v2.0.0 pools each entry additionally + * carries `fastInboundRateLimiterState` / `fastOutboundRateLimiterState`, the separate buckets + * applied to Faster-Than-Finality and safe-finality (FCR) transfers. + * @remarks Every pool-version difference is handled for you — v1.5.0's singular `getRemotePool` + * vs v1.5.1+'s `getRemotePools`, and a `USDCTokenPoolProxy`'s indirection through its underlying + * pools. Reads only; to change a lane use the lane-configuration write ops. + * @remarks The Solana counterpart, `SolanaTokenManager.getTokenPoolRemotes`, returns this same + * {@link TokenPoolRemote} shape, but is addressed differently: it takes the token `mint` plus a + * pool program, where this takes the pool contract address directly. * @throws {@link CCTParamsInvalidError} if `poolAddress` is not a valid address, or * `remoteChainSelector` is given and is not a `uint64` - * @throws {@link CCIPTokenPoolChainConfigNotFoundError} if a lane read has no remote token - * configured — including a `remoteChainSelector` the pool knows nothing about + * @throws {@link CCIPTokenPoolChainConfigNotFoundError} if a scanned lane has no remote token + * configured * @example * ```typescript + * // every configured lane * const remotes = await cct.getTokenPoolRemotes({ poolAddress: '0xPool...' }) * for (const [network, lane] of Object.entries(remotes)) { - * // inboundRateLimiterState is null when inbound transfers are unlimited - * console.log(network, lane.remoteToken, lane.inboundRateLimiterState?.capacity) + * const inbound = lane.inboundRateLimiterState + * console.log(network, lane.remoteToken, lane.remotePools, inbound?.capacity ?? 'unlimited') * } + * + * // or just one, avoiding a full scan + * const one = await cct.getTokenPoolRemotes({ + * poolAddress: '0xPool...', + * remoteChainSelector: 5009297550715157269n, // ethereum-mainnet + * }) * ``` */ getTokenPoolRemotes(opts: GetTokenPoolRemotesParams): Promise { @@ -981,10 +1096,18 @@ export type { ApplyChainUpdatesParamsV1_5_1, ChainUpdateV1_5_0, ChainUpdateV1_5_1, - RateLimitConfigInput, } from './token-pool/operations/apply-chain-updates.ts' -/** The lane types `GetTokenPoolRemotesResult` is keyed over; shared with `Chain.getTokenPoolRemotes`. */ +/** + * `GetTokenPoolRemotesResult` is a `Record`, so a caller cannot name a + * single lane's type without these. Declared in `../../chain.ts` (shared with the core + * `Chain.getTokenPoolRemotes`), re-exported here so this entry point is self-sufficient. + */ export type { RateLimiterState, TokenPoolRemote } from '../../chain.ts' +export type { + ChainRateLimitUpdate, + SetChainRateLimiterConfigsParams, +} from './token-pool/operations/set-chain-rate-limiter-configs.ts' +export type { RateLimitConfig } from './token-pool/rate-limit.ts' export * from './token-pool/contracts.ts' export type { DeployLockboxParams } from './lockbox/operations/deploy-lockbox.ts' export type { AuthorizeLockboxCallersParams } from './lockbox/operations/authorize-callers.ts' diff --git a/ccip-sdk/src/cct/evm/token-pool/contracts.ts b/ccip-sdk/src/cct/evm/token-pool/contracts.ts index 2be8a8445..1916c865c 100644 --- a/ccip-sdk/src/cct/evm/token-pool/contracts.ts +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.ts @@ -1,11 +1,11 @@ /** - * EVM token-pool contract metadata for CCT, and the reads that resolve it. Families, types and - * versions; the cached {@link Interface}s built from the vendored ABIs; on-chain type/version - * resolution ({@link resolveTokenPool}, {@link parseTokenPoolVersion}, - * {@link getTokenPoolInterface}); the deployable pools' creation artifacts - * ({@link getTokenPoolArtifact}); the version dispatch the write ops encode through - * ({@link resolveEncoder}); and the owner pre-flight they share ({@link assertPoolOwner}). - * Mirrors `token/contracts.ts`. + * 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}), the narrow role reads every owner-gated write pre-flights + * `sender` against ({@link readTokenPoolOwner}, {@link readTokenPoolRateLimitAdmin}) plus the + * owner-only guard built on the first of them ({@link assertPoolOwner}). The write-side + * rate-limit shape lane-config ops share lives in `rate-limit.ts`. Mirrors `token/contracts.ts`. * * @packageDocumentation */ @@ -124,7 +124,9 @@ export function parseTokenPoolVersion({ if (!isTokenPoolType(contractType)) throw new CCTContractTypeInvalidError(address, TOKEN_POOL_TYPES.join(', '), contractType) if (!isTokenPoolVersion(version)) - throw new CCTContractVersionUnsupportedError(contractType, version, { context: { address } }) + throw new CCTContractVersionUnsupportedError(contractType, version, { + context: { address }, + }) return { type: contractType, version } } @@ -143,10 +145,7 @@ export async function resolveTokenPool( return parseTokenPoolVersion({ address, contractType, version }) } -/** - * `Ownable2Step.owner()`, declared identically by every supported pool type and version — so one - * vendored ABI reads the owner of any of them, with no per-generation dispatch. - */ +/** `Ownable2Step.owner()`, identical across all supported pool types and versions. */ type PoolOwnerGetter = Pick, 'owner'> /** @@ -172,12 +171,7 @@ export async function assertPoolOwner( poolAddress: string, sender: string, ): Promise { - const pool: PoolOwnerGetter = getTypedContract( - chain, - poolAddress, - BURN_MINT_TOKEN_POOL_V1_5_0_ABI, - ) - const owner = getAddress(resultToObject(await pool.owner())) + const owner = await readTokenPoolOwner(chain, poolAddress) if (getAddress(sender) === owner) return throw new CCTParamsInvalidError( operation, @@ -215,6 +209,75 @@ export function getTokenPoolInterface(type: TokenPoolType, version: TokenPoolVer return TOKEN_POOL_INTERFACES[getTokenPoolFamily(type)][version] } +/** + * Reads a token pool's Ownable2Step `owner()` in a single `eth_call`. The one owner read every + * owner-gated pool write op pre-flights `sender` against. + * + * @remarks No `version` parameter and no family dispatch: `owner()` is declared identically — + * same selector, same `address` return — by both {@link TOKEN_POOL_FAMILIES} at all four + * supported versions, so the v1.5.0 `BurnMint` interface types the call for every pool. + * @remarks **Deliberately not routed through the `getTokenPoolState` query op, and must not be + * "simplified" back to it.** Two reasons, the first of which is a correctness bug and not just a + * cost concern: + * + * 1. `getTokenPoolState` throws {@link CCTContractTypeInvalidError} for a v2.0.0 + * `SiloedLockReleaseTokenPool`, because that pool escrows per remote chain + * (`getLockBox(uint64)`) and so has no single `lockBox` field for the query's result shape to + * report. `SiloedLockReleaseTokenPool` is nonetheless a supported {@link TokenPoolType}, and + * the write ops' calldata is perfectly valid against it. Gating an owner check through that + * query would therefore make every one of those ops permanently unusable on siloed pools — + * failing on an unrelated result-shape limitation while `generateUnsigned*` works fine. + * 2. It costs 6–8 `eth_call`s (token, router, RMN proxy, rate-limit admin, supported chains, + * dynamic config, finality config, lockbox) plus a `getTokenInfo` round trip, and re-resolves + * `typeAndVersion`, all to obtain one address. + * + * This mirrors `token-admin-registry/operations/transfer-admin.ts`, which likewise does its own + * narrow pre-tx read rather than going through a read op. + * @param chain - Chain to read from. + * @param poolAddress - Token pool contract to read `owner()` from. + * @returns The current owner, checksummed. + */ +export async function readTokenPoolOwner(chain: EVMChain, poolAddress: string): Promise { + const pool: PoolOwnerGetter = getTypedContract( + chain, + poolAddress, + BURN_MINT_TOKEN_POOL_V1_5_0_ABI, + ) + return getAddress(resultToObject(await pool.owner())) +} + +/** + * Reads a token pool's `rateLimitAdmin` — the delegated role the pools accept for rate-limit + * writes alongside the owner — in a single `eth_call`. + * + * @remarks Same rationale as {@link readTokenPoolOwner} for not routing through + * `getTokenPoolState`. + * @remarks Version-dispatched, unlike `owner()`: v1.5.0–v1.6.1 expose a standalone + * `getRateLimitAdmin()`, while v2.0.0 folded the role into `getDynamicConfig()`'s + * `(router, rateLimitAdmin, feeAdmin)` triple. + * @param chain - Chain to read from. + * @param poolAddress - Token pool contract to read from. + * @param version - Pool version, as resolved by {@link resolveTokenPool}; selects the getter. + * @returns The current rate-limit admin, checksummed. The zero address when the role is unset — + * callers must treat that as "matches nobody" rather than comparing it directly. + */ +export async function readTokenPoolRateLimitAdmin( + chain: EVMChain, + poolAddress: string, + version: TokenPoolVersion, +): Promise { + if (version === TokenPoolVersion.V2_0_0) { + const pool = getTypedContract(chain, poolAddress, BURN_MINT_TOKEN_POOL_V2_0_0_ABI) + // getDynamicConfig returns (router, rateLimitAdmin, feeAdmin); index the raw Result rather + // than resultToObject it, which would turn the named tuple into an object (see + // get-token-pool-state.ts). + const dynamicConfig = await pool.getDynamicConfig() + return getAddress(dynamicConfig[1] as string) + } + const pool = getTypedContract(chain, poolAddress, BURN_MINT_TOKEN_POOL_V1_5_1_ABI) + return getAddress(resultToObject(await pool.getRateLimitAdmin())) +} + /** * 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 @@ -254,10 +317,8 @@ export function getTokenPoolArtifact(type: DeployableTokenPoolType): DeployArtif * A table entry says one of two things: * - **absent key** — the calldata did not change here, so it *inherits* the closest lower entry. * One entry per calldata change therefore covers every higher version. - * - **explicit `null`** — the function is gone from this version up. The walk stops instead of - * inheriting downwards, and the op is reported unsupported. Floor-match alone is only sound for - * functions that survive; without a ceiling, a removed function's older encoder would emit - * calldata for a selector the pool does not implement. + * - **explicit `null`** — the function was removed at this version, so the op is reported + * unsupported rather than emitting calldata for a selector the pool does not implement. * * @param encoders - Sparse table keyed by {@link TokenPoolVersion}; `null` marks a removal ceiling. * @param version - The resolved on-chain pool version to encode for. diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.ts b/ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.ts index 8256f970c..0e16d5a9d 100644 --- a/ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.ts +++ b/ccip-sdk/src/cct/evm/token-pool/operations/apply-chain-updates.ts @@ -23,7 +23,6 @@ import { validateArray, validateBoolean, validateNonZeroAddress, - validateUint128, validateUint64, } from '../../validate.ts' import { @@ -33,6 +32,11 @@ import { resolveEncoder, resolveTokenPool, } from '../contracts.ts' +import { + type ParsedRateLimitConfig, + type RateLimitConfig, + parseRateLimitConfig, +} from '../rate-limit.ts' // --------------------------------------------------------------------------- // Shared @@ -47,92 +51,6 @@ export type ApplyChainUpdatesParamVersion = | typeof TokenPoolVersion.V1_5_0 | typeof TokenPoolVersion.V1_5_1 -/** - * One direction of a token pool rate limiter, as callers write it. Amounts are in the token's - * smallest unit: at 6 decimals, `1_000_000n` is one token. - * - * @remarks Field-for-field the Solana `RateLimitConfig` in - * `cct/solana/token-pool/operations/set-chain-rate-limit.ts`, so cross-family callers write one - * shape; only the bound differs (EVM `uint128`, Solana `u64`). Hence the discriminant is spelled - * **`enabled`** rather than the ABI's `isEnabled`; {@link parseRateLimitConfig} resolves it to - * {@link RateLimitConfig}. Distinct from the read-side `RateLimiterState` in `chain.ts`, which also - * reports the live `tokens` balance. - */ -export type RateLimitConfigInput = - | { - /** Whether this directional rate limit is enforced. */ - enabled: true - /** Maximum token amount in the bucket (`uint128`); must be at least `rate`. */ - capacity: bigint - /** - * Token amount restored to the bucket per second (`uint128`); at most `capacity`, which - * v1.5.0/v1.5.1 tighten to `0 < rate < 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 - } - -/** - * One direction of a rate limiter as the ABI spells it: a {@link RateLimitConfigInput} with its - * optional amounts resolved to concrete `bigint`s and its discriminant re-keyed. A parsed lane is - * therefore a `ChainUpdate` struct verbatim, so the encoders need no re-keying pass. - */ -export type RateLimitConfig = { - /** Whether this directional rate limit is enforced (the ABI's spelling of `enabled`). */ - isEnabled: boolean - /** Maximum token amount in the bucket (`uint128`); zero when disabled. */ - capacity: bigint - /** Token amount restored to the bucket per second (`uint128`); zero when disabled. */ - rate: bigint -} - -/** - * Validates one direction of a rate limiter and fills in its omitted amounts. Every rule here is - * version-independent, so it runs before the first RPC; the version-conditional - * `0 < rate < capacity` bound waits for {@link ApplyChainUpdates.assertRateBounds}. `direction` is - * this direction's param path, so failures report as `${direction}.rate`. - * @throws {@link CCTParamsInvalidError} if `config` is not a valid rate-limit configuration - */ -function parseRateLimitConfig( - operation: string, - direction: string, - config: unknown, -): RateLimitConfig { - const input = parseRecord(operation, direction, config, 'rate-limit configuration') - const { enabled } = input - validateBoolean(operation, `${direction}.enabled`, enabled) - - // Only a disabled direction defaults: an enabled one must state both amounts, so an omitted - // amount stays `undefined` and is rejected by the uint128 check below, under its own path. - const capacity = !enabled && input.capacity === undefined ? 0n : input.capacity - const rate = !enabled && input.rate === undefined ? 0n : input.rate - validateUint128(operation, `${direction}.capacity`, capacity) - validateUint128(operation, `${direction}.rate`, rate) - - 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 { isEnabled: enabled, capacity, rate } -} - /** The lane fields both parameter shapes share, and which encode identically. */ type ChainUpdateCommon = { /** CCIP selector of the remote chain (`uint64`). */ @@ -140,9 +58,9 @@ type ChainUpdateCommon = { /** Hex-encoded remote token address, `0x` prefix optional; must be non-empty whole bytes. */ remoteTokenAddress: string /** Rate limit for tokens received from the remote chain. */ - inboundRateLimiterConfig: RateLimitConfigInput + inboundRateLimiterConfig: RateLimitConfig /** Rate limit for tokens sent to the remote chain. */ - outboundRateLimiterConfig: RateLimitConfigInput + outboundRateLimiterConfig: RateLimitConfig } /** The top-level parameters both shapes share; each version adds its own lane arrays. */ @@ -159,8 +77,8 @@ type ApplyChainUpdatesBaseParams = { /** A lane with its rate limits resolved — derived, so the parsed and public shapes cannot drift. */ type WithParsedRateLimits = Omit & { - inboundRateLimiterConfig: RateLimitConfig - outboundRateLimiterConfig: RateLimitConfig + inboundRateLimiterConfig: ParsedRateLimitConfig + outboundRateLimiterConfig: ParsedRateLimitConfig } /** @@ -200,7 +118,7 @@ function parseLaneSelector( return selector } -/** Parses the lane fields both shapes share, in the order failures should be reported. */ +/** Parses the lane fields both shapes share. */ function parseLaneCommon( operation: string, path: string, @@ -225,11 +143,13 @@ function parseLaneCommon( operation, `${path}.inboundRateLimiterConfig`, update.inboundRateLimiterConfig, + null, ), outboundRateLimiterConfig: parseRateLimitConfig( operation, `${path}.outboundRateLimiterConfig`, update.outboundRateLimiterConfig, + null, ), } } @@ -286,7 +206,7 @@ function parseChainsV1_5_0(operation: string, chains: unknown) { const stillEnabled = !allowed && (['inboundRateLimiterConfig', 'outboundRateLimiterConfig'] as const).find( - (direction) => lane[direction].isEnabled, + (direction) => lane[direction].enabled, ) if (stillEnabled) { throw new CCTParamsInvalidError( @@ -299,12 +219,30 @@ function parseChainsV1_5_0(operation: string, chains: unknown) { }) } -/** Encodes the v1.5.0 signature: one `chains` array, each lane carrying its own `allowed` bit. */ +/** Encodes the v1.5.0 signature. */ const encodeV1_5_0 = ( iface: Interface, params: ParsedApplyChainUpdatesParamsV1_5_0, ): UnsignedEVMTx => - callTx(params.poolAddress, iface.encodeFunctionData('applyChainUpdates', [params.chains])) + callTx( + params.poolAddress, + iface.encodeFunctionData('applyChainUpdates', [ + params.chains.map((lane) => ({ + ...lane, + // re-key the shared `enabled` to the ABI's `isEnabled` + inboundRateLimiterConfig: (({ enabled: isEnabled, capacity, rate }) => ({ + isEnabled, + capacity, + rate, + }))(lane.inboundRateLimiterConfig), + outboundRateLimiterConfig: (({ enabled: isEnabled, capacity, rate }) => ({ + isEnabled, + capacity, + rate, + }))(lane.outboundRateLimiterConfig), + })), + ]), + ) // --------------------------------------------------------------------------- // v1.5.1+ @@ -371,7 +309,7 @@ function parseChainsV1_5_1( return { chainsToAdd: adds, remoteChainSelectorsToRemove: removals } } -/** Encodes the v1.5.1+ signature: removals first, then the lanes to add. */ +/** Encodes the v1.5.1+ signature. */ const encodeV1_5_1 = ( iface: Interface, params: ParsedApplyChainUpdatesParamsV1_5_1, @@ -380,7 +318,20 @@ const encodeV1_5_1 = ( params.poolAddress, iface.encodeFunctionData('applyChainUpdates', [ params.remoteChainSelectorsToRemove, - params.chainsToAdd, + params.chainsToAdd.map((lane) => ({ + ...lane, + // re-key the shared `enabled` to the ABI's `isEnabled` + inboundRateLimiterConfig: (({ enabled: isEnabled, capacity, rate }) => ({ + isEnabled, + capacity, + rate, + }))(lane.inboundRateLimiterConfig), + outboundRateLimiterConfig: (({ enabled: isEnabled, capacity, rate }) => ({ + isEnabled, + capacity, + rate, + }))(lane.outboundRateLimiterConfig), + })), ]), ) @@ -423,20 +374,21 @@ export type ApplyChainUpdatesParamsV1_5_1 = ApplyChainUpdatesBaseParams & { } /** - * {@link ApplyChainUpdatesParams} as {@link ApplyChainUpdates.parse} leaves it: selectors - * range-checked, remote addresses normalised to lower-case `0x` hex, and rate limits resolved to - * concrete amounts keyed as the ABI spells them. The encoders add no validation of their own — a - * parsed lane is already a `ChainUpdate` struct, so they only choose the argument order. + * {@link ApplyChainUpdatesParams} as {@link ApplyChainUpdates.parse} leaves it. The encoders add + * no validation of their own — a parsed lane is already a `ChainUpdate` struct. */ type ParsedApplyChainUpdatesParams = | ParsedApplyChainUpdatesParamsV1_5_0 | ParsedApplyChainUpdatesParamsV1_5_1 /** Encodes parsed params into `applyChainUpdates` calldata, widened over the parsed union. */ -type Encoder = (iface: Interface, params: ParsedApplyChainUpdatesParams) => UnsignedEVMTx +type EncodeFn = (iface: Interface, params: ParsedApplyChainUpdatesParams) => UnsignedEVMTx -/** One {@link ApplyChainUpdates.encoders} entry: the shape it accepts, and the encoder for it. */ -type EncoderEntry = { shape: ApplyChainUpdatesParamVersion; encode: Encoder } +/** One {@link ApplyChainUpdates.encoders} entry: the shape it accepts, and the {@link EncodeFn} for it. */ +type Encoder = { + shape: V + encode: EncodeFn +} /** * Configures, enables and disables a token pool's remote lanes via `applyChainUpdates`. @@ -451,14 +403,17 @@ export class ApplyChainUpdates extends EVMOperation< > { readonly name = 'applyChainUpdates' - /** - * Encoder per pool version, floor-matched; v1.6.1 and v2.0.0 inherit v1.5.1's. The cast holds - * only while {@link buildUnsigned} checks `shape` against `params.version` before encoding. - */ + /** Encoder per pool version, floor-matched; v1.6.1 and v2.0.0 inherit v1.5.1's. */ private readonly encoders = { - [TokenPoolVersion.V1_5_0]: { shape: TokenPoolVersion.V1_5_0, encode: encodeV1_5_0 }, - [TokenPoolVersion.V1_5_1]: { shape: TokenPoolVersion.V1_5_1, encode: encodeV1_5_1 }, - } as Partial> + [TokenPoolVersion.V1_5_0]: { + shape: TokenPoolVersion.V1_5_0, + encode: encodeV1_5_0, + }, + [TokenPoolVersion.V1_5_1]: { + shape: TokenPoolVersion.V1_5_1, + encode: encodeV1_5_1, + }, + } as { [V in ApplyChainUpdatesParamVersion]?: Encoder } /** * Validates the pool address and every lane entry before any RPC, *keeping* what each check @@ -471,7 +426,10 @@ export class ApplyChainUpdates extends EVMOperation< const version: string = params.version switch (params.version) { case TokenPoolVersion.V1_5_0: - return { ...params, chains: parseChainsV1_5_0(this.name, params.chains) } + return { + ...params, + chains: parseChainsV1_5_0(this.name, params.chains), + } case TokenPoolVersion.V1_5_1: return { ...params, @@ -481,22 +439,18 @@ export class ApplyChainUpdates extends EVMOperation< throw new CCTParamsInvalidError( this.name, 'version', - `must be one of ${TokenPoolVersion.V1_5_0}, ${TokenPoolVersion.V1_5_1}, got ${String(version)}`, + `must be one of ${TokenPoolVersion.V1_5_0}, ${ + TokenPoolVersion.V1_5_1 + }, got ${String(version)}`, ) } } /** - * Applies the version-conditional rate bound — the only lane rule outside {@link parse}, because - * it needs the version `resolveTokenPool` has just reported. - * - * @remarks On v1.5.0/v1.5.1, `RateLimiter._validateTokenBucketConfig` reverts - * `InvalidRateLimitRate` on `rate >= capacity || rate == 0`, so an enabled bucket needs - * `0 < rate < capacity`. v1.6.1 and v2.0.0 reject only `rate > capacity`, so there - * `rate === capacity` and a zero rate are legitimate and must NOT be rejected. + * Applies the version-conditional rate bound, which needs the version `resolveTokenPool` has + * just reported, via the shared {@link parseRateLimitConfig}. */ private assertRateBounds(params: ParsedApplyChainUpdatesParams, version: TokenPoolVersion): void { - if (version !== TokenPoolVersion.V1_5_0 && version !== TokenPoolVersion.V1_5_1) return const lanes = params.version === TokenPoolVersion.V1_5_0 ? params.chains.map((lane, i) => [`chains[${i}]`, lane] as const) @@ -504,13 +458,9 @@ export class ApplyChainUpdates extends EVMOperation< for (const [path, lane] of lanes) { for (const direction of ['inboundRateLimiterConfig', 'outboundRateLimiterConfig'] as const) { - const { isEnabled, capacity, rate } = lane[direction] - if (!isEnabled || (rate > 0n && rate < capacity)) continue - throw new CCTParamsInvalidError( - this.name, - `${path}.${direction}.rate`, - `must be greater than zero and strictly less than capacity when enabled on a v${version} pool, which reverts InvalidRateLimitRate otherwise (v1.6.1 and later allow rate == capacity and a zero rate)`, - ) + // already parsed to the shared `enabled` shape; re-running with the resolved version + // applies the version-conditional bound + parseRateLimitConfig(this.name, `${path}.${direction}`, lane[direction], version) } } } @@ -528,7 +478,12 @@ export class ApplyChainUpdates extends EVMOperation< ): Promise { const { type, version } = await resolveTokenPool(chain, params.poolAddress) - const { shape, encode } = resolveEncoder(this.encoders, version, this.name) + // explicit type argument: inference would otherwise fix `F` to the first entry's `shape` + const { shape, encode } = resolveEncoder>( + this.encoders, + version, + this.name, + ) if (params.version !== shape) throw new CCTParamsInvalidError( this.name, diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.test.ts new file mode 100644 index 000000000..824cc27c9 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.test.ts @@ -0,0 +1,635 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { parseTypeAndVersion } from '../../../../utils.ts' +import { CCTParamsInvalidError } from '../../../errors.ts' +import { type TokenPoolFamily, TOKEN_POOL_INTERFACES, TokenPoolVersion } from '../contracts.ts' +import { + type ChainRateLimitUpdate, + type SetChainRateLimiterConfigsParams, + SetChainRateLimiterConfigs, +} from './set-chain-rate-limiter-configs.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 HASH = '0x' + 'ab'.repeat(32) + +const ETHEREUM = 5009297550715157269n +const SEPOLIA = 16015286601757825753n + +/** Two lanes: one enabled with amounts, one disabled with its amounts omitted. */ +const UPDATES: ChainRateLimitUpdate[] = [ + { + remoteChainSelector: ETHEREUM, + outboundRateLimiterConfig: { enabled: true, capacity: 1_000_000n, rate: 100n }, + inboundRateLimiterConfig: { enabled: true, capacity: 2_000_000n, rate: 200n }, + }, + { + remoteChainSelector: SEPOLIA, + outboundRateLimiterConfig: { enabled: false }, + inboundRateLimiterConfig: { enabled: false }, + }, +] + +/** + * Expected v1.5.1/v1.6.1 calldata, from a FRESH Interface written off the human-readable + * signature — never the SDK's own cached one, which would make this a tautology. + */ +const BATCH_DATA = new Interface([ + 'function setChainRateLimiterConfigs(uint64[] remoteChainSelectors, (bool isEnabled, uint128 capacity, uint128 rate)[] outboundConfigs, (bool isEnabled, uint128 capacity, uint128 rate)[] inboundConfigs)', +]).encodeFunctionData('setChainRateLimiterConfigs', [ + [ETHEREUM, SEPOLIA], + [ + [true, 1_000_000n, 100n], + [false, 0n, 0n], + ], + [ + [true, 2_000_000n, 200n], + [false, 0n, 0n], + ], +]) + +/** Expected v2.0.0 calldata, likewise from a fresh Interface; `fastFinality` defaults to false. */ +const v2Data = (fastFinality: [boolean, boolean] = [false, false]) => + new Interface([ + 'function setRateLimitConfig((uint64 remoteChainSelector, bool fastFinality, (bool isEnabled, uint128 capacity, uint128 rate) outboundRateLimiterConfig, (bool isEnabled, uint128 capacity, uint128 rate) inboundRateLimiterConfig)[] rateLimitConfigArgs)', + ]).encodeFunctionData('setRateLimitConfig', [ + [ + [ETHEREUM, fastFinality[0], [true, 1_000_000n, 100n], [true, 2_000_000n, 200n]], + [SEPOLIA, fastFinality[1], [false, 0n, 0n], [false, 0n, 0n]], + ], + ]) + +/** + * Expected v1.5.0 calldata, from a fresh Interface off the singular signature. v1.5.0 sets one + * lane per call, so this carries only the first of the two {@link UPDATES} lanes. + */ +const SINGLE_DATA_V1_5_0 = new Interface([ + 'function setChainRateLimiterConfig(uint64 remoteChainSelector, (bool isEnabled, uint128 capacity, uint128 rate) outboundConfig, (bool isEnabled, uint128 capacity, uint128 rate) inboundConfig)', +]).encodeFunctionData('setChainRateLimiterConfig', [ + ETHEREUM, + [true, 1_000_000n, 100n], + [true, 2_000_000n, 200n], +]) + +/** Getters the role check reads, as `functionName -> return values` (ABI-encoded on demand). */ +type Reads = Record + +/** v2.0.0 pool state: the rate-limit role comes out of `getDynamicConfig`. */ +const readsV2_0_0 = (rateLimitAdmin = RATE_LIMIT_ADMIN, owner = OWNER): Reads => ({ + getToken: [TOKEN], + owner: [owner], + getRmnProxy: [RMN_PROXY], + getTokenDecimals: [18], + getSupportedChains: [[ETHEREUM, SEPOLIA]], + getDynamicConfig: [ROUTER, rateLimitAdmin, FEE_ADMIN], + getAllowedFinalityConfig: ['0x00000000'], +}) + +/** + * Legacy (pre-2.0.0) pool state: the rate-limit role has its own standalone `getRateLimitAdmin()` + * getter, decoded as a bare address rather than out of a `getDynamicConfig` triple. + */ +const readsLegacy = (rateLimitAdmin = RATE_LIMIT_ADMIN, owner = OWNER): Reads => ({ + getToken: [TOKEN], + owner: [owner], + getRouter: [ROUTER], + getRmnProxy: [RMN_PROXY], + getRateLimitAdmin: [rateLimitAdmin], + getSupportedChains: [[ETHEREUM, SEPOLIA]], +}) + +/** + * Just the two getters `buildUnsigned`'s owner-or-rateLimitAdmin pre-flight reads, per version + * generation. The default for {@link stubChain}: since the role check moved out of `execute` and + * into `buildUnsigned`, every `generate` with a `sender` needs them answered. + */ +const roleReads = ( + version: TokenPoolVersion, + { + owner = OWNER, + rateLimitAdmin = RATE_LIMIT_ADMIN, + }: { owner?: string; rateLimitAdmin?: string } = {}, +): Reads => + version === TokenPoolVersion.V2_0_0 + ? { owner: [owner], getDynamicConfig: [ROUTER, rateLimitAdmin, FEE_ADMIN] } + : { owner: [owner], getRateLimitAdmin: [rateLimitAdmin] } + +const POOL_TYPE: Record = { + BurnMint: 'BurnMintTokenPool', + LockRelease: 'LockReleaseTokenPool', +} + +/** + * EVMChain stub: `typeAndVersion` reports the pool's own, and the provider answers `eth_call` + * from `reads`, keyed by selector off that version's Interface. Any getter absent from `reads` + * reverts, so a test that supplies none proves no RPC read was attempted. + */ +function stubChain({ + family = 'BurnMint', + version = TokenPoolVersion.V2_0_0, + reads = roleReads(version), + onCall, +}: { + family?: TokenPoolFamily + version?: TokenPoolVersion + reads?: Reads + onCall?: () => void +} = {}): 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 }) => { + onCall?.() + 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: () => { + onCall?.() + return Promise.resolve(parseTypeAndVersion(`${POOL_TYPE[family]} ${version}`)) + }, + getTokenInfo: () => Promise.resolve({ decimals: 18, symbol: 'TKN', name: 'Token' }), + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(address = OWNER, waitError?: Error) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + 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 SetChainRateLimiterConfigs() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { + poolAddress: POOL, + updates: UPDATES, + sender: OWNER, + ...overrides, + }) +} + +describe('SetChainRateLimiterConfigs (cct/evm)', () => { + describe('generate', () => { + for (const family of ['BurnMint', 'LockRelease'] as const) { + for (const version of [TokenPoolVersion.V1_5_1, TokenPoolVersion.V1_6_1] as const) { + it(`encodes the batch setChainRateLimiterConfigs for a ${version} ${family} pool`, async () => { + const unsigned = await generate(stubChain({ family, version })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, POOL) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, BATCH_DATA) + }) + } + + it(`encodes setRateLimitConfig for a 2.0.0 ${family} pool`, async () => { + const unsigned = await generate(stubChain({ family, version: TokenPoolVersion.V2_0_0 })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, POOL) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, v2Data()) + }) + } + + it('encodes identically for the BurnMint and LockRelease families', async () => { + for (const version of [ + TokenPoolVersion.V1_5_1, + TokenPoolVersion.V1_6_1, + TokenPoolVersion.V2_0_0, + ] as const) { + const burnMint = await generate(stubChain({ family: 'BurnMint', version })) + const lockRelease = await generate(stubChain({ family: 'LockRelease', version })) + assert.equal(burnMint.transactions[0]!.data, lockRelease.transactions[0]!.data) + } + }) + + it('carries per-entry fastFinality on a 2.0.0 pool', async () => { + const unsigned = await generate(stubChain({ version: TokenPoolVersion.V2_0_0 }), { + updates: [ + { ...UPDATES[0]!, fastFinality: true }, + { ...UPDATES[1]!, fastFinality: false }, + ], + }) + assert.equal(unsigned.transactions[0]!.data, v2Data([true, false])) + }) + + it('omits from when sender is not supplied', async () => { + const unsigned = await generate(stubChain(), { sender: undefined }) + assert.equal(unsigned.transactions[0]!.from, undefined) + }) + }) + + /** + * The offline / multisig builder is gated on the same owner-OR-rateLimitAdmin disjunction as + * `execute`. Before this lived in `buildUnsigned`, `generateUnsignedSetChainRateLimiterConfigs` + * with an arbitrary `sender` issued zero `eth_call`s and handed back a fully-formed transaction + * with an unauthorized `from` — which reverts `Unauthorized` only after review and signing. + */ + describe('generate role pre-flight', () => { + for (const version of [ + TokenPoolVersion.V1_5_1, + TokenPoolVersion.V1_6_1, + TokenPoolVersion.V2_0_0, + ] as const) { + it(`rejects a sender that is neither the owner nor the rateLimitAdmin on v${version}`, async () => { + await assert.rejects( + () => generate(stubChain({ version }), { sender: '0x' + '88'.repeat(20) }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setChainRateLimiterConfigs' && + err.context.param === 'sender', + ) + }) + + it(`accepts the rateLimitAdmin as sender on v${version}`, async () => { + const unsigned = await generate(stubChain({ version }), { sender: RATE_LIMIT_ADMIN }) + assert.equal(unsigned.transactions[0]!.from, RATE_LIMIT_ADMIN) + }) + } + + it('does not let a zero-address sender match an unset rateLimitAdmin', async () => { + await assert.rejects( + () => + generate( + stubChain({ + reads: roleReads(TokenPoolVersion.V2_0_0, { rateLimitAdmin: ZeroAddress }), + }), + { + sender: ZeroAddress, + }, + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setChainRateLimiterConfigs' && + err.context.param === 'sender', + ) + }) + + it('skips the role reads entirely when sender is omitted', async () => { + let calls = 0 + // no `owner`/`getDynamicConfig` answers at all: any role read would revert + const unsigned = await generate(stubChain({ reads: {}, onCall: () => calls++ }), { + sender: undefined, + }) + assert.equal(unsigned.transactions[0]!.data, v2Data()) + assert.equal(calls, 1, 'only the typeAndVersion probe') + }) + }) + + describe('validation', () => { + const cases: { + name: string + param: string + overrides: Partial + /** Set when only the version-specific encoder can reject it (so one RPC is expected). */ + version?: TokenPoolVersion + }[] = [ + { name: 'an invalid poolAddress', param: 'poolAddress', overrides: { poolAddress: 'nope' } }, + // a tx to `0x0` hits no code, so it would mine as a successful no-op rather than reverting + { + name: 'the zero poolAddress', + param: 'poolAddress', + overrides: { poolAddress: ZeroAddress }, + }, + // `.map` skips holes, so without the density guard this used to reach ethers as `undefined` + { + name: 'a hole in updates', + param: 'updates[1]', + overrides: { + updates: (() => { + const sparse = [UPDATES[0]!] + sparse[2] = UPDATES[1]! + return sparse + })(), + }, + }, + { name: 'an invalid sender', param: 'sender', overrides: { sender: 'nope' } }, + { name: 'empty updates', param: 'updates', overrides: { updates: [] } }, + { + name: 'a non-array updates', + param: 'updates', + overrides: { updates: undefined }, + }, + { + name: 'a duplicate remoteChainSelector', + param: 'updates[1].remoteChainSelector', + overrides: { updates: [UPDATES[0]!, { ...UPDATES[1]!, remoteChainSelector: ETHEREUM }] }, + }, + { + name: 'a non-uint64 remoteChainSelector', + param: 'updates[0].remoteChainSelector', + overrides: { updates: [{ ...UPDATES[0]!, remoteChainSelector: -1n }] }, + }, + { + name: 'rate above capacity while enabled', + param: 'updates[0].outboundRateLimiterConfig.rate', + overrides: { + updates: [ + { + ...UPDATES[0]!, + outboundRateLimiterConfig: { enabled: true, capacity: 10n, rate: 11n }, + }, + ], + }, + }, + { + name: 'a non-zero capacity while disabled', + param: 'updates[0].inboundRateLimiterConfig', + overrides: { + updates: [{ ...UPDATES[0]!, inboundRateLimiterConfig: { enabled: false, capacity: 1n } }], + }, + }, + { + name: 'a missing enabled discriminant', + param: 'updates[0].inboundRateLimiterConfig.enabled', + overrides: { + updates: [ + { + ...UPDATES[0]!, + inboundRateLimiterConfig: + {} as unknown as ChainRateLimitUpdate['inboundRateLimiterConfig'], + }, + ], + }, + }, + { + name: 'a non-boolean fastFinality', + param: 'updates[0].fastFinality', + overrides: { + updates: [{ ...UPDATES[0]!, fastFinality: 'yes' as unknown as boolean }], + }, + }, + { + name: 'fastFinality on a pre-2.0.0 pool', + param: 'updates[0].fastFinality', + overrides: { updates: [{ ...UPDATES[0]!, fastFinality: true }] }, + version: TokenPoolVersion.V1_5_1, + }, + { + name: 'fastFinality: false on a pre-2.0.0 pool', + param: 'updates[0].fastFinality', + overrides: { updates: [{ ...UPDATES[0]!, fastFinality: false }] }, + version: TokenPoolVersion.V1_5_1, + }, + ] + + for (const { name, param, overrides, version } of cases) { + it(`rejects ${name}`, async () => { + let calls = 0 + await assert.rejects( + () => generate(stubChain({ version, onCall: () => calls++ }), overrides), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setChainRateLimiterConfigs' && + err.context.param === param, + ) + // params-only failures short-circuit before any RPC; the version-gated ones need exactly + // the one `typeAndVersion` probe that resolved the encoder + assert.equal(calls, version === undefined ? 0 : 1) + }) + } + }) + + describe('version dispatch', () => { + it('encodes the singular setChainRateLimiterConfig for a 1.5.0 pool, one lane per tx', async () => { + const unsigned = await generate(stubChain({ version: TokenPoolVersion.V1_5_0 }), { + updates: [UPDATES[0]!], + }) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, POOL) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, SINGLE_DATA_V1_5_0) + }) + + it('rejects a multi-lane batch on a 1.5.0 pool rather than fanning out to N transactions', async () => { + await assert.rejects( + () => generate(stubChain({ version: TokenPoolVersion.V1_5_0 })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setChainRateLimiterConfigs' && + err.context.param === 'updates', + ) + }) + + for (const version of [ + TokenPoolVersion.V1_5_1, + TokenPoolVersion.V1_6_1, + TokenPoolVersion.V2_0_0, + ] as const) { + it(`supports a ${version} pool`, async () => { + const unsigned = await generate(stubChain({ version })) + assert.equal( + unsigned.transactions[0]!.data, + version === TokenPoolVersion.V2_0_0 ? v2Data() : BATCH_DATA, + ) + }) + } + }) + + describe('execute', () => { + const params = { poolAddress: POOL, updates: UPDATES } + + it('signs and submits as the pool owner, resolving to the tx hash', async () => { + assert.deepEqual( + await op.execute(stubChain({ reads: readsV2_0_0() }), { + ...params, + wallet: fakeSigner(OWNER), + }), + { hash: HASH }, + ) + }) + + it('accepts the rateLimitAdmin as well as the owner', async () => { + assert.deepEqual( + await op.execute(stubChain({ reads: readsV2_0_0() }), { + ...params, + wallet: fakeSigner(RATE_LIMIT_ADMIN), + }), + { hash: HASH }, + ) + }) + + it('accepts a legacy (1.5.1) pool, reading its standalone getRateLimitAdmin', async () => { + assert.deepEqual( + await op.execute( + stubChain({ + version: TokenPoolVersion.V1_5_1, + reads: { + getToken: [TOKEN], + owner: [OWNER], + getRouter: [ROUTER], + getRmnProxy: [RMN_PROXY], + getRateLimitAdmin: [RATE_LIMIT_ADMIN], + getSupportedChains: [[ETHEREUM, SEPOLIA]], + }, + }), + { ...params, wallet: fakeSigner(RATE_LIMIT_ADMIN) }, + ), + { hash: HASH }, + ) + }) + + it('rejects a sender that is neither the owner nor the rateLimitAdmin', async () => { + await assert.rejects( + () => + op.execute(stubChain({ reads: readsV2_0_0() }), { + ...params, + wallet: fakeSigner('0x' + '88'.repeat(20)), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setChainRateLimiterConfigs' && + err.context.param === 'sender', + ) + }) + + it('does not let a zero-address sender match an unset rateLimitAdmin', async () => { + await assert.rejects( + () => + op.execute(stubChain({ reads: readsV2_0_0(ZeroAddress) }), { + ...params, + wallet: fakeSigner(ZeroAddress), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setChainRateLimiterConfigs' && + err.context.param === 'sender', + ) + }) + + it('rejects a sender that differs from the signing wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain({ reads: readsV2_0_0() }), { + ...params, + sender: RATE_LIMIT_ADMIN, + wallet: fakeSigner(OWNER), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setChainRateLimiterConfigs' && + err.context.param === 'sender', + ) + }) + + it('maps an on-chain revert to CCIPExecTxRevertedError', async () => { + await assert.rejects( + () => + op.execute(stubChain({ reads: readsV2_0_0() }), { + ...params, + wallet: fakeSigner(OWNER, makeError('execution reverted', 'CALL_EXCEPTION')), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && + err.context.operation === 'setChainRateLimiterConfigs', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain({ reads: readsV2_0_0() }), { ...params, wallet: {} }), + CCIPWalletInvalidError, + ) + }) + }) + + /** + * The legacy `getRateLimitAdmin()` branch of the role read. Every case in `execute` above runs + * on a 2.0.0 stub, where `rateLimitAdmin` is instead decoded out of `getDynamicConfig()`'s + * `(router, rateLimitAdmin, feeAdmin)` triple — so the pre-2.0.0 getter and its single-address + * decode would otherwise have no coverage, including the zero-address guard. + */ + describe('execute on a legacy (pre-2.0.0) pool', () => { + const params = { poolAddress: POOL, updates: UPDATES } + const legacyPool = (reads: Reads) => stubChain({ version: TokenPoolVersion.V1_6_1, reads }) + + it('accepts the rateLimitAdmin read from the standalone getRateLimitAdmin()', async () => { + assert.deepEqual( + await op.execute(legacyPool(readsLegacy()), { + ...params, + wallet: fakeSigner(RATE_LIMIT_ADMIN), + }), + { hash: HASH }, + ) + }) + + it('accepts the pool owner', async () => { + assert.deepEqual( + await op.execute(legacyPool(readsLegacy()), { ...params, wallet: fakeSigner(OWNER) }), + { hash: HASH }, + ) + }) + + it('rejects a sender that is neither the owner nor the rateLimitAdmin', async () => { + await assert.rejects( + () => + op.execute(legacyPool(readsLegacy()), { + ...params, + wallet: fakeSigner('0x' + '88'.repeat(20)), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setChainRateLimiterConfigs' && + err.context.param === 'sender', + ) + }) + + it('does not let a zero-address sender match an unset rateLimitAdmin', async () => { + await assert.rejects( + () => + op.execute(legacyPool(readsLegacy(ZeroAddress)), { + ...params, + wallet: fakeSigner(ZeroAddress), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setChainRateLimiterConfigs' && + err.context.param === 'sender', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.ts b/ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.ts new file mode 100644 index 000000000..098b7ccf4 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.ts @@ -0,0 +1,352 @@ +/** + * setChainRateLimiterConfigs — sets the inbound/outbound rate limits of one or more configured + * lanes on a token pool, in a single transaction. + * + * @remarks Every supported version is served by its own entry point, keeping the + * one-op-one-transaction invariant every CCT write holds: v1.5.1/v1.6.1 encode the batch + * `setChainRateLimiterConfigs(uint64[], Config[], Config[])`, v2.0.0 the reshaped + * `setRateLimitConfig(RateLimitConfigArgs[])`, and v1.5.0 — which ships only the singular + * `setChainRateLimiterConfig(uint64, Config, Config)` — that call. Because v1.5.0 sets one lane + * per transaction, a v1.5.0 pool accepts only a single-element `updates`; a multi-lane batch is + * rejected with {@link CCTParamsInvalidError} rather than fanned out into N transactions. + * + * @packageDocumentation + */ + +import { type 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 { assertDenseArray, validateNonZeroAddress, validateUint64 } from '../../validate.ts' +import { + TokenPoolVersion, + getTokenPoolInterface, + readTokenPoolOwner, + readTokenPoolRateLimitAdmin, + resolveEncoder, + resolveTokenPool, +} from '../contracts.ts' +import { + type ParsedRateLimitConfig, + type RateLimitConfig, + parseRateLimitConfig, +} from '../rate-limit.ts' + +/** + * New rate limits for one already-configured lane. + * + * @remarks The two config fields are deliberately spelled `inboundRateLimiterConfig` / + * `outboundRateLimiterConfig`, matching the Solana `ChainUpdate` in + * `cct/solana/token-pool/operations/apply-chain-updates.ts`, so the very same config objects can be + * passed to `applyChainUpdates` and to this op. + * + * This op only *updates* limits — it does not add a lane. A selector the pool has no chain config + * for reverts on-chain (`NonExistentChain`); add it first with the pool's chain-update op. + */ +export type ChainRateLimitUpdate = { + /** CCIP selector of the already-configured remote chain (`uint64`). */ + remoteChainSelector: bigint + /** Limit on tokens received from the remote chain. */ + inboundRateLimiterConfig: RateLimitConfig + /** Limit on tokens sent to the remote chain. */ + outboundRateLimiterConfig: RateLimitConfig + /** + * Whether this entry configures the lane's *fast-finality* (FTF) buckets rather than its + * finalized ones. **v2.0.0 only** — the field does not exist in the pre-2.0.0 ABIs, so setting it + * (to either value) on a v1.5.1/v1.6.1 pool is rejected instead of silently dropped. Defaults to + * `false` on v2.0.0. + */ + fastFinality?: boolean +} + +/** Parameters for {@link SetChainRateLimiterConfigs}. */ +export type SetChainRateLimiterConfigsParams = { + /** Token pool contract whose lane limits are being set. */ + poolAddress: string + /** Lanes to re-limit; at least one, with no repeated `remoteChainSelector`. */ + updates: ChainRateLimitUpdate[] + /** + * Pool `owner` or `rateLimitAdmin` — the two roles the pools accept for rate-limit writes. Sets + * `tx.from` for offline / multisig signing; {@link SetChainRateLimiterConfigs.execute} + * additionally checks it on-chain. + */ + sender?: string +} + +/** A {@link ChainRateLimitUpdate} with both directions parsed and its `fastFinality` resolved. */ +type ParsedChainRateLimitUpdate = { + remoteChainSelector: bigint + inbound: ParsedRateLimitConfig + outbound: ParsedRateLimitConfig + fastFinality: boolean +} + +/** + * Validates `updates` and resolves every entry: non-empty, selectors distinct `uint64`s, both + * directions through {@link parseRateLimitConfig}. + * @param operation - Operation name, for the error context. + * @param updates - The caller-supplied value, unvalidated. + * @param allowFastFinality - Whether the resolved pool version has the per-entry `fastFinality` + * flag (v2.0.0 and up). When `false`, an entry that sets it at all is rejected. + * @param version - Resolved pool version, or `null` pre-RPC, which skips the stricter + * v1.5.0/v1.5.1 rate bound. + * @throws {@link CCTParamsInvalidError} if `updates` is not a non-empty array, an entry is not an + * object, a selector repeats or is not a `uint64`, `fastFinality` is not a boolean (or is set on a + * version without it), or either direction's config is invalid for `version` + */ +function parseUpdates( + operation: string, + updates: unknown, + allowFastFinality: boolean, + version: TokenPoolVersion | null, +): ParsedChainRateLimitUpdate[] { + if (!Array.isArray(updates) || updates.length === 0) + throw new CCTParamsInvalidError(operation, 'updates', 'must be a non-empty array') + // `.map` below skips holes, so reject a sparse array before it can smuggle one past validation + assertDenseArray(operation, 'updates', updates) + + const seen = new Set() + return updates.map((update, i) => { + const path = `updates[${i}]` + if (typeof update !== 'object' || update === null) + throw new CCTParamsInvalidError(operation, path, 'must be a chain rate-limit update') + + const { + remoteChainSelector, + inboundRateLimiterConfig, + outboundRateLimiterConfig, + fastFinality, + } = update as Partial + + validateUint64(operation, `${path}.remoteChainSelector`, remoteChainSelector) + if (seen.has(remoteChainSelector)) + throw new CCTParamsInvalidError( + operation, + `${path}.remoteChainSelector`, + `is a duplicate of an earlier update (${remoteChainSelector}); each lane may appear only once`, + ) + seen.add(remoteChainSelector) + + if (fastFinality !== undefined) { + if (!allowFastFinality) + throw new CCTParamsInvalidError( + operation, + `${path}.fastFinality`, + 'is only supported from pool version 2.0.0; omit it for older pools', + ) + if (typeof fastFinality !== 'boolean') + throw new CCTParamsInvalidError(operation, `${path}.fastFinality`, 'must be a boolean') + } + + return { + remoteChainSelector, + inbound: parseRateLimitConfig( + operation, + `${path}.inboundRateLimiterConfig`, + inboundRateLimiterConfig, + version, + ), + outbound: parseRateLimitConfig( + operation, + `${path}.outboundRateLimiterConfig`, + outboundRateLimiterConfig, + version, + ), + fastFinality: fastFinality ?? false, + } + }) +} + +/** + * Encodes the batch rate-limit call against the resolved pool {@link Interface}. + * @remarks `version` is the pool's *actual* resolved version, not the encoder's floor: the v1.5.1 + * encoder serves both v1.5.1 and v1.6.1, whose enabled-bucket rate bounds differ, so it has to be + * told which one it is encoding for. + */ +type Encoder = ( + iface: Interface, + params: SetChainRateLimiterConfigsParams, + version: TokenPoolVersion, +) => UnsignedEVMTx + +/** The on-chain `RateLimiter.Config` tuple: `enabled` maps to the ABI's `isEnabled`. */ +type RateLimiterConfigTuple = [isEnabled: boolean, capacity: bigint, rate: bigint] + +const toTuple = ({ enabled, capacity, rate }: ParsedRateLimitConfig): RateLimiterConfigTuple => [ + enabled, + capacity, + rate, +] + +/** + * v1.5.0: `setChainRateLimiterConfig(uint64, Config outbound, Config inbound)` — the singular + * entry point, one lane per call, so a v1.5.0 pool accepts only a single-element `updates`. A + * multi-lane batch is rejected here rather than fanned out into N transactions, which would break + * the one-op-one-transaction invariant. No `fastFinality` at this version. + */ +const encodeSingleConfigV1_5_0: Encoder = (iface, { poolAddress, updates }, version) => { + const parsed = parseUpdates('setChainRateLimiterConfigs', updates, false, version) + if (parsed.length !== 1) + throw new CCTParamsInvalidError( + 'setChainRateLimiterConfigs', + 'updates', + `must contain exactly one lane on a v1.5.0 pool, which sets rate limits one lane per transaction (got ${parsed.length}); split the batch into one call per lane`, + ) + const [update] = parsed + return callTx( + poolAddress, + iface.encodeFunctionData('setChainRateLimiterConfig', [ + update!.remoteChainSelector, + toTuple(update!.outbound), + toTuple(update!.inbound), + ]), + ) +} + +/** + * v1.5.1/v1.6.1: `setChainRateLimiterConfigs(uint64[], Config[] outbound, Config[] inbound)` — + * three parallel arrays, outbound before inbound. No `fastFinality` at these versions. + */ +const encodeBatchConfigs: Encoder = (iface, { poolAddress, updates }, version) => { + const parsed = parseUpdates('setChainRateLimiterConfigs', updates, false, version) + return callTx( + poolAddress, + iface.encodeFunctionData('setChainRateLimiterConfigs', [ + parsed.map((u) => u.remoteChainSelector), + parsed.map((u) => toTuple(u.outbound)), + parsed.map((u) => toTuple(u.inbound)), + ]), + ) +} + +/** + * v2.0.0: `setRateLimitConfig(RateLimitConfigArgs[])` — one struct per lane, folding the selector + * and the new `fastFinality` flag in with the two configs (outbound before inbound). + */ +const encodeRateLimitConfigV2_0_0: Encoder = (iface, { poolAddress, updates }, version) => { + const parsed = parseUpdates('setChainRateLimiterConfigs', updates, true, version) + return callTx( + poolAddress, + iface.encodeFunctionData('setRateLimitConfig', [ + parsed.map((u) => [ + u.remoteChainSelector, + u.fastFinality, + toTuple(u.outbound), + toTuple(u.inbound), + ]), + ]), + ) +} + +/** + * Sets the inbound/outbound rate limits of one or more configured lanes on a token pool, in a + * single transaction. Gated on the pool's `owner` **or** its `rateLimitAdmin`. + */ +export class SetChainRateLimiterConfigs extends EVMOperation { + readonly name = 'setChainRateLimiterConfigs' + + /** + * One entry per calldata shape: v1.5.0 has only the singular call, v1.6.1 inherits the v1.5.1 + * batch encoding, and v2.0.0 renamed and reshaped the call, hence its own entry. + */ + private readonly encoders: Partial> = { + [TokenPoolVersion.V1_5_0]: encodeSingleConfigV1_5_0, + [TokenPoolVersion.V1_5_1]: encodeBatchConfigs, + [TokenPoolVersion.V2_0_0]: encodeRateLimitConfigV2_0_0, + } + + /** + * Validates the pool address and every update before any RPC. `fastFinality` is *permitted* + * here, and the version-specific rate bounds are not applied (`null` version) — whether this + * pool has that field, and which bound its `RateLimiter` enforces, are only known once its + * version is resolved, so the version-specific encoder is what rejects those (see + * {@link parseUpdates}). + */ + protected override validate({ poolAddress, updates }: SetChainRateLimiterConfigsParams): void { + validateNonZeroAddress(this.name, 'poolAddress', poolAddress) + parseUpdates(this.name, updates, true, null) + } + + /** + * Reads the pool's type-and-version, floor-matches the encoder and its contract interface, then + * — when `sender` is known — pre-flights it against the pool's `owner` **or** its + * `rateLimitAdmin`. + * + * @remarks The role check lives here, not only in {@link execute}, so the offline / multisig + * path gets it too: `generateUnsignedSetChainRateLimiterConfigs` with an unauthorized `sender` + * would otherwise hand back a fully-formed transaction that reverts `Unauthorized` only after + * being reviewed and signed. Every sibling pool write gates in `buildUnsigned` for the same + * reason; this one is gated on a *disjunction* rather than the owner alone, so it reads both + * roles instead of using `assertPoolOwner`. + * @remarks Ordered *after* the encoder so a bad parameter fails on the one `typeAndVersion` + * probe rather than after two more role reads. + * @throws {@link CCTParamsInvalidError} if `sender` is neither the pool `owner` nor its (set) + * `rateLimitAdmin`, or a multi-lane `updates` is sent to a v1.5.0 pool + */ + protected async buildUnsigned( + chain: EVMChain, + params: SetChainRateLimiterConfigsParams, + ): Promise { + const { type, version } = await resolveTokenPool(chain, params.poolAddress) + const encode = resolveEncoder(this.encoders, version, this.name) + const unsigned = encode(getTokenPoolInterface(type, version), params, version) + if (params.sender !== undefined) + await this.#assertRateLimitRole(chain, params.poolAddress, params.sender, version) + return unsigned + } + + /** + * Rejects a `sender` that is neither the pool's `owner` nor its `rateLimitAdmin`. + * + * @remarks `rateLimitAdmin` is unset on most pools, where it reads as the zero address, so it is + * only compared once known to be *set*: an equality-first check would let a zero-address `sender` + * match an unset admin and authorize a transaction nobody can send. + * @remarks `version` selects which getter reports `rateLimitAdmin` (standalone pre-2.0.0, folded + * into `getDynamicConfig` at 2.0.0). Both roles are read directly, not via the + * `getTokenPoolState` query op — see {@link readTokenPoolOwner} for why. + * @throws {@link CCTParamsInvalidError} if `sender` holds neither role + */ + async #assertRateLimitRole( + chain: EVMChain, + poolAddress: string, + sender: string, + version: TokenPoolVersion, + ): Promise { + const [owner, rateLimitAdmin] = await Promise.all([ + readTokenPoolOwner(chain, poolAddress), + readTokenPoolRateLimitAdmin(chain, poolAddress, version), + ]) + + const signer = getAddress(sender) + // an unset rateLimitAdmin is the zero address; exclude it before comparing or a zero-address + // `sender` would match it + const isRateLimitAdmin = rateLimitAdmin !== ZeroAddress && rateLimitAdmin === signer + if (owner !== signer && !isRateLimitAdmin) { + throw new CCTParamsInvalidError( + this.name, + 'sender', + `must be the pool owner (${owner})${rateLimitAdmin === ZeroAddress ? ' — this pool has no rateLimitAdmin set' : ` or its rateLimitAdmin (${rateLimitAdmin})`}`, + ) + } + } + + /** + * Signs and submits, binding `sender` to the signing wallet's address — see + * {@link EVMOperation.resolveWalletSender} for why a divergent `sender` is rejected rather than + * signed. The owner-or-`rateLimitAdmin` gate is {@link buildUnsigned}'s. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTParamsInvalidError} if any param is invalid, or `sender` is neither the + * wallet's address, the pool `owner`, nor the pool's (set) `rateLimitAdmin`, or a multi-lane + * `updates` is sent to a v1.5.0 pool + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/rate-limit.test.ts b/ccip-sdk/src/cct/evm/token-pool/rate-limit.test.ts new file mode 100644 index 000000000..92d87fe35 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/rate-limit.test.ts @@ -0,0 +1,240 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { CCTParamsInvalidError } from '../../errors.ts' +import { TokenPoolVersion } from './contracts.ts' +import { parseRateLimitConfig } from './rate-limit.ts' + +describe('parseRateLimitConfig', () => { + const UINT128_MAX = 2n ** 128n - 1n + + it('returns an enabled config unchanged', () => { + assert.deepEqual( + parseRateLimitConfig( + 'op', + 'inboundRateLimiterConfig', + { enabled: true, capacity: 100n, rate: 10n }, + null, + ), + { enabled: true, capacity: 100n, rate: 10n }, + ) + }) + + it('allows rate to equal capacity when enabled, where the version permits it', () => { + // `null` (version not yet resolved) and the two relaxed versions; the strict ones are + // covered in `version-specific enabled-bucket bounds` below + for (const version of [null, TokenPoolVersion.V1_6_1, TokenPoolVersion.V2_0_0] as const) { + assert.deepEqual( + parseRateLimitConfig( + 'op', + 'inboundRateLimiterConfig', + { enabled: true, capacity: 10n, rate: 10n }, + version, + ), + { enabled: true, capacity: 10n, rate: 10n }, + `version ${String(version)}`, + ) + } + }) + + it('accepts uint128 max for both amounts', () => { + assert.deepEqual( + parseRateLimitConfig( + 'op', + 'inboundRateLimiterConfig', + { enabled: true, capacity: UINT128_MAX, rate: UINT128_MAX }, + null, + ), + { enabled: true, capacity: UINT128_MAX, rate: UINT128_MAX }, + ) + }) + + it('defaults omitted amounts to zero when disabled', () => { + assert.deepEqual( + parseRateLimitConfig('op', 'outboundRateLimiterConfig', { enabled: false }, null), + { enabled: false, capacity: 0n, rate: 0n }, + ) + }) + + it('accepts explicit zeros when disabled', () => { + assert.deepEqual( + parseRateLimitConfig( + 'op', + 'outboundRateLimiterConfig', + { enabled: false, capacity: 0n, rate: 0n }, + null, + ), + { enabled: false, capacity: 0n, rate: 0n }, + ) + }) + + it('reports failures with dotted param paths under the direction', () => { + const cases: Array<[unknown, string]> = [ + [undefined, 'inboundRateLimiterConfig'], + [null, 'inboundRateLimiterConfig'], + ['enabled', 'inboundRateLimiterConfig'], + [{}, 'inboundRateLimiterConfig.enabled'], + [{ enabled: 'yes' }, 'inboundRateLimiterConfig.enabled'], + // enabled with a missing amount: no defaulting applies, so the bound check reports it + [{ enabled: true, rate: 1n }, 'inboundRateLimiterConfig.capacity'], + [{ enabled: true, capacity: 1n }, 'inboundRateLimiterConfig.rate'], + [{ enabled: true, capacity: 1, rate: 1n }, 'inboundRateLimiterConfig.capacity'], + [{ enabled: true, capacity: -1n, rate: 0n }, 'inboundRateLimiterConfig.capacity'], + [ + { enabled: true, capacity: UINT128_MAX + 1n, rate: 0n }, + 'inboundRateLimiterConfig.capacity', + ], + [{ enabled: true, capacity: 0n, rate: -1n }, 'inboundRateLimiterConfig.rate'], + // rate above capacity is only an error while enabled + [{ enabled: true, capacity: 10n, rate: 11n }, 'inboundRateLimiterConfig.rate'], + // disabled must be all-zero, and the whole direction is blamed + [{ enabled: false, capacity: 1n }, 'inboundRateLimiterConfig'], + [{ enabled: false, rate: 1n }, 'inboundRateLimiterConfig'], + ] + + for (const [config, param] of cases) { + assert.throws( + () => parseRateLimitConfig('op', 'inboundRateLimiterConfig', config, null), + (error: unknown) => + error instanceof CCTParamsInvalidError && + error.context.operation === 'op' && + error.context.param === param, + `expected ${JSON.stringify(String(param))} for ${String(JSON.stringify(config, (_k, v) => (typeof v === 'bigint' ? String(v) : v)))}`, + ) + } + }) + + /** + * The enabled-bucket bound is version-dependent, and getting this wrong in either direction is a + * bug: + * + * - v1.5.0/v1.5.1 `RateLimiter._validateTokenBucketConfig` reverts `InvalidRateLimitRate` when + * `config.rate >= config.capacity || config.rate == 0`, so an enabled config needs + * `0 < rate < capacity` — calldata that violates it always reverts, and must fail locally. + * - v1.6.1/v2.0.0 relaxed that to `config.rate > config.capacity`, so `rate === capacity` and + * `rate === 0n` are *legitimate* there. Tightening the rule globally would be a new bug, which + * is what the accept-side cases below pin. + */ + describe('version-specific enabled-bucket bounds', () => { + const STRICT = [TokenPoolVersion.V1_5_0, TokenPoolVersion.V1_5_1] as const + const RELAXED = [TokenPoolVersion.V1_6_1, TokenPoolVersion.V2_0_0] as const + + for (const version of STRICT) { + it(`rejects rate === capacity when enabled on v${version}`, () => { + assert.throws( + () => + parseRateLimitConfig( + 'op', + 'outboundRateLimiterConfig', + { enabled: true, capacity: 10n, rate: 10n }, + version, + ), + (error: unknown) => + error instanceof CCTParamsInvalidError && + error.context.operation === 'op' && + error.context.param === 'outboundRateLimiterConfig.rate', + ) + }) + + it(`rejects a zero rate when enabled on v${version}`, () => { + assert.throws( + () => + parseRateLimitConfig( + 'op', + 'inboundRateLimiterConfig', + { enabled: true, capacity: 10n, rate: 0n }, + version, + ), + (error: unknown) => + error instanceof CCTParamsInvalidError && + error.context.operation === 'op' && + error.context.param === 'inboundRateLimiterConfig.rate', + ) + }) + + it(`still accepts 0 < rate < capacity on v${version}`, () => { + assert.deepEqual( + parseRateLimitConfig( + 'op', + 'inboundRateLimiterConfig', + { enabled: true, capacity: 10n, rate: 9n }, + version, + ), + { enabled: true, capacity: 10n, rate: 9n }, + ) + }) + + /** + * The version-independent `rate > capacity` bound must survive the strict tightening. If a + * refactor ever made the strict branch *replace* the base check rather than follow it, + * `rate > capacity` would become accepted on exactly the two versions that revert hardest + * on it — so this asserts both that it still throws and that it is the *base* message doing + * the throwing, which is what proves the order. + */ + it(`still rejects rate > capacity when enabled on v${version}`, () => { + assert.throws( + () => + parseRateLimitConfig( + 'op', + 'outboundRateLimiterConfig', + { enabled: true, capacity: 10n, rate: 11n }, + version, + ), + (error: unknown) => + error instanceof CCTParamsInvalidError && + error.context.operation === 'op' && + error.context.param === 'outboundRateLimiterConfig.rate' && + error.message.includes('must not exceed capacity when enabled'), + ) + }) + + it(`does not apply the strict rule to a disabled config on v${version}`, () => { + assert.deepEqual( + parseRateLimitConfig('op', 'inboundRateLimiterConfig', { enabled: false }, version), + { enabled: false, capacity: 0n, rate: 0n }, + ) + }) + } + + for (const version of RELAXED) { + it(`accepts rate === capacity when enabled on v${version}`, () => { + assert.deepEqual( + parseRateLimitConfig( + 'op', + 'outboundRateLimiterConfig', + { enabled: true, capacity: 10n, rate: 10n }, + version, + ), + { enabled: true, capacity: 10n, rate: 10n }, + ) + }) + + it(`accepts a zero rate when enabled on v${version}`, () => { + assert.deepEqual( + parseRateLimitConfig( + 'op', + 'inboundRateLimiterConfig', + { enabled: true, capacity: 10n, rate: 0n }, + version, + ), + { enabled: true, capacity: 10n, rate: 0n }, + ) + }) + + it(`still rejects rate > capacity when enabled on v${version}`, () => { + assert.throws( + () => + parseRateLimitConfig( + 'op', + 'inboundRateLimiterConfig', + { enabled: true, capacity: 10n, rate: 11n }, + version, + ), + (error: unknown) => + error instanceof CCTParamsInvalidError && + error.context.param === 'inboundRateLimiterConfig.rate', + ) + }) + } + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/rate-limit.ts b/ccip-sdk/src/cct/evm/token-pool/rate-limit.ts new file mode 100644 index 000000000..c7a8f689f --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/rate-limit.ts @@ -0,0 +1,162 @@ +/** + * The write-side rate-limit shape every EVM lane-config op shares, and its validation. + * + * @remarks Pulled out of `contracts.ts` (which is pool *contract metadata* — type/version + * resolution, cached interfaces, deploy artifacts) and out of the individual ops, so the + * caller-facing {@link RateLimitConfig} shape and the version-conditional enabled-bucket bound + * have exactly one definition. `applyChainUpdates` and `setChainRateLimiterConfigs` both build on + * this; each keeps its own parsed output shape. + * + * @packageDocumentation + */ + +import { CCTParamsInvalidError } from '../../errors.ts' +import { parseRecord, validateBoolean, validateUint128 } from '../validate.ts' +import { TokenPoolVersion } from './contracts.ts' + +/** + * Configuration for one direction of a token pool rate limiter, as CCT callers write it. + * + * @remarks Field-for-field identical to the Solana `RateLimitConfig` in + * `cct/solana/token-pool/operations/set-chain-rate-limit.ts`, so cross-family callers write one + * shape; only the bound differs (EVM `uint128` here, Solana `u64` there). + * + * The discriminant is spelled **`enabled`**, not the ABI's `isEnabled`: matching the Solana op's + * public field name matters more than matching the ABI, because callers write cross-family code + * against the SDK. Ops map `enabled` → `isEnabled` when building the on-chain + * `RateLimiter.Config` tuple. + * + * Distinct from the read-side `RateLimiterState` in `chain.ts`, which additionally carries + * the live `tokens` bucket balance — that is what a pool *reports*, this is what a caller *sets*. + * + * For a token 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 (`uint128`). Must be at least `rate` on v1.6.1/v2.0.0 + * pools, and strictly greater than `rate` on v1.5.0/v1.5.1 — see {@link RateLimitConfig.rate}. + */ + capacity: bigint + /** + * Token amount restored to the bucket per second (`uint128`). + * + * The bound the contracts enforce is **version-dependent**, so this is checked against the + * resolved pool version rather than one global rule: + * - **v1.6.1 / v2.0.0** — `rate <= capacity`. `rate === capacity` and `rate === 0n` are both + * legal (`RateLimiter._validateTokenBucketConfig` only reverts `InvalidRateLimitRate` when + * `rate > capacity`). + * - **v1.5.0 / v1.5.1** — stricter: `0n < rate < capacity`. Those versions revert when + * `rate >= capacity || rate == 0`, so a config that is fine on a newer pool is rejected here. + */ + 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 + } + +/** + * A {@link RateLimitConfig} with its optional amounts resolved to concrete `bigint`s, still keyed + * `enabled`. Ops that encode the ABI's `isEnabled` re-key at the tuple boundary. + */ +export type ParsedRateLimitConfig = { + enabled: boolean + capacity: bigint + rate: bigint +} + +/** + * The versions whose `RateLimiter._validateTokenBucketConfig` rejects an enabled bucket unless + * `0 < rate < capacity`: + * + * ```solidity + * if (config.isEnabled) { if (config.rate >= config.capacity || config.rate == 0) revert InvalidRateLimitRate(config); } + * ``` + * + * v1.6.1 and v2.0.0 relaxed that to `if (config.rate > config.capacity) revert ...`, so on those + * versions `rate === capacity` and `rate === 0n` are legitimate and must NOT be rejected. + */ +const STRICT_RATE_BOUND_VERSIONS: readonly TokenPoolVersion[] = [ + TokenPoolVersion.V1_5_0, + TokenPoolVersion.V1_5_1, +] + +/** + * Validates one direction of a rate limiter and fills in its omitted amounts, mirroring the Solana + * `parseRateLimitConfig` with `uint128` bounds. + * + * @remarks `version` is required-and-nullable (not optional) so each call site states explicitly + * whether the version-specific bound applies: pre-RPC `validate()` passes `null` (version-independent + * checks only, so bad params still fail before the first `eth_call`), while version-specific + * encoders pass the resolved {@link TokenPoolVersion} and get the tightening. The bound changed + * between pool generations — see {@link STRICT_RATE_BOUND_VERSIONS}. + * @param operation - Operation name, for the error context. + * @param direction - Param path of this direction (e.g. `inboundRateLimiterConfig`); nested + * failures report as `${direction}.capacity` / `${direction}.rate` / `${direction}.enabled`. + * @param config - The caller-supplied value, unvalidated. + * @param version - Resolved pool version, or `null` when it is not known yet (pre-RPC validation), + * which applies the version-independent checks alone. + * @returns The direction with `capacity`/`rate` defaulted to `0n` when disabled and omitted. + * @throws {@link CCTParamsInvalidError} if `config` is not an object, `enabled` is not a boolean, + * either amount is not a `uint128`, `rate` exceeds `capacity` while enabled, `rate` is zero or + * equal to `capacity` while enabled on a v1.5.0/v1.5.1 pool, or either amount is non-zero while + * disabled + */ +export function parseRateLimitConfig( + operation: string, + direction: string, + config: unknown, + version: TokenPoolVersion | null, +): ParsedRateLimitConfig { + const input = parseRecord(operation, direction, config, 'rate-limit configuration') + const { enabled } = input + validateBoolean(operation, `${direction}.enabled`, enabled) + + // Only a disabled direction defaults: an enabled one must state both amounts, so an omitted + // amount stays `undefined` and is rejected by the uint128 check below, under its own path. + const capacity = !enabled && input.capacity === undefined ? 0n : input.capacity + const rate = !enabled && input.rate === undefined ? 0n : input.rate + validateUint128(operation, `${direction}.capacity`, capacity) + validateUint128(operation, `${direction}.rate`, rate) + + if (enabled && rate > capacity) { + throw new CCTParamsInvalidError( + operation, + `${direction}.rate`, + 'must not exceed capacity when enabled', + ) + } + // Version-specific tightening, applied ONLY where the contract itself is stricter: v1.5.0 and + // v1.5.1 revert `InvalidRateLimitRate` for an enabled bucket unless `0 < rate < capacity`. + if (enabled && version !== null && STRICT_RATE_BOUND_VERSIONS.includes(version)) { + if (rate === 0n) { + throw new CCTParamsInvalidError( + operation, + `${direction}.rate`, + `must be greater than zero when enabled on a v${version} pool, which reverts InvalidRateLimitRate on a zero rate`, + ) + } + if (rate === capacity) { + throw new CCTParamsInvalidError( + operation, + `${direction}.rate`, + `must be strictly less than capacity when enabled on a v${version} pool, which reverts InvalidRateLimitRate on rate == capacity (v1.6.1 and later allow it)`, + ) + } + } + if (!enabled && (capacity !== 0n || rate !== 0n)) { + throw new CCTParamsInvalidError( + operation, + direction, + 'must have zero capacity and rate when disabled', + ) + } + return { enabled, capacity, rate } +} diff --git a/ccip-sdk/src/cct/evm/validate.ts b/ccip-sdk/src/cct/evm/validate.ts index 33e80a6d3..39f8221b9 100644 --- a/ccip-sdk/src/cct/evm/validate.ts +++ b/ccip-sdk/src/cct/evm/validate.ts @@ -109,8 +109,7 @@ export function validateUint256(operation: string, param: string, value: unknown /** * Asserts `value` is a `bigint` in `[0, 2^128 − 1]` (a Solidity `uint128`), narrowing it to - * `bigint` for callers — where {@link validateUint256} returns `void`, because callers that - * default an omitted amount to `0n` hold a `bigint | undefined` this has to resolve. + * `bigint` for callers. * @throws {@link CCTParamsInvalidError} if `value` is not such a bigint */ export function validateUint128( @@ -137,9 +136,15 @@ export function validateUint64( /** * Parses an optionally `0x`-prefixed hex string of whole, non-empty bytes into the 0x-prefixed * lower-case form ethers encodes as `bytes`. - * @remarks No byte cap, unlike Solana's counterpart: the values this guards are *remote* addresses - * carried as `bytes`, and a remote may be Solana or Aptos (32 bytes) as easily as EVM (20), so a - * length ceiling would only reject valid remotes. + * + * @remarks The EVM counterpart of Solana's `parseHexBytes`/`parseNonEmptyHexBytes`, minus their + * byte cap: the values this guards are *remote* addresses carried as `bytes` (a lane's remote + * token or remote pool), and a remote may be Solana or Aptos (32 bytes) as easily as EVM (20), so + * a length ceiling here would only reject valid remotes. Shared by the remote-pool write ops and + * `applyChainUpdates`, which previously each carried their own copy of this parser. + * @param operation - Operation name, for the error context. + * @param param - Param path to blame, e.g. `remotePoolAddress` or `chains[0].remoteTokenAddress`. + * @param value - The caller-supplied value, unvalidated. * @returns The value as `0x`-prefixed lower-case hex. * @throws {@link CCTParamsInvalidError} if `value` is not a non-empty whole-byte hex string */ @@ -232,3 +237,33 @@ export function parseUniqueHexBytesArray( return hex }) } + +/** + * Asserts `value` — already known to be an array — has no holes. + * + * @remarks Every array param in this package is validated element-wise with `.forEach`/`.map`, + * and both of those **skip holes**. A sparse array (`const a = [x]; a[2] = y`) therefore walks + * through element validation untouched, and the hole reaches ABI encoding as `undefined`, where + * ethers throws a bare `TypeError` carrying none of the `operation`/`param` context this package + * promises — and only *after* the `typeAndVersion` RPC has been spent. Checking density up front + * keeps the "fail before RPC, with an indexed param path" contract intact. + * @param operation - Operation name, for the error context. + * @param param - Param path of the array itself, e.g. `chainsToAdd`; the blamed index is appended. + * @param value - The array to check. + * @throws {@link CCTParamsInvalidError} naming the first hole, e.g. `chainsToAdd[1]` + */ +export function assertDenseArray( + operation: string, + param: string, + value: readonly unknown[], +): void { + for (let i = 0; i < value.length; i++) { + if (!(i in value)) { + throw new CCTParamsInvalidError( + operation, + `${param}[${i}]`, + 'must not be a hole — the array is sparse, and a missing element cannot be encoded', + ) + } + } +} From c0fb13a2229af55c360c3f5d33c077c2ba8c1dcf Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:31:56 +0100 Subject: [PATCH 77/87] feat(cct-sdk): Add set rate limit admin + dynamic config ops (#394) * feat(cct-sdk): Add get token pool remotes evm query * feat(cct-sdk): Add generic EVM param validators * feat(cct-sdk): Read siloed lock/release pool state * feat(cct-sdk): Add pool owner pre-flight and encoder removal ceilings * refactor(cct-sdk): Hoist validate/parse lifecycle to the shared Operation base * feat(cct-sdk): Add apply chain updates evm op * fix(cct-sdk): Reject a declared version that is not the pool's calldata shape * feat(cct-sdk): Add EVM remote pool ops * Address PR comments * feat(cct-sdk): Add set chain rate limiter configs op * feat(cct-sdk): Add set rate limit admin and set dynamic config ops * Fix lint * Fix lint * Fix lint * Fix lint * Fix tsdoc and lint * cleanup * linting --- ccip-sdk/src/cct/evm/index.ts | 137 +++++++++ .../operations/set-dynamic-config.test.ts | 284 ++++++++++++++++++ .../operations/set-dynamic-config.ts | 163 ++++++++++ .../operations/set-rate-limit-admin.test.ts | 254 ++++++++++++++++ .../operations/set-rate-limit-admin.ts | 132 ++++++++ 5 files changed, 970 insertions(+) create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/set-dynamic-config.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/set-dynamic-config.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/set-rate-limit-admin.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/set-rate-limit-admin.ts diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index ca6f54899..2276cf019 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -70,6 +70,14 @@ import { type SetChainRateLimiterConfigsParams, SetChainRateLimiterConfigs, } from './token-pool/operations/set-chain-rate-limiter-configs.ts' +import { + type SetDynamicConfigParams, + SetDynamicConfig, +} from './token-pool/operations/set-dynamic-config.ts' +import { + type SetRateLimitAdminParams, + SetRateLimitAdmin, +} from './token-pool/operations/set-rate-limit-admin.ts' import { type SetRemotePoolParams, SetRemotePool } from './token-pool/operations/set-remote-pool.ts' import { type TransferOwnershipParams, @@ -101,6 +109,8 @@ export class EVMTokenManager extends TokenManager { readonly #removeRemotePool = new RemoveRemotePool() readonly #applyChainUpdates = new ApplyChainUpdates() readonly #setChainRateLimiterConfigs = new SetChainRateLimiterConfigs() + readonly #setRateLimitAdmin = new SetRateLimitAdmin() + readonly #setDynamicConfig = new SetDynamicConfig() // Lockbox operations readonly #deployLockbox = new DeployLockbox() @@ -486,6 +496,133 @@ export class EVMTokenManager extends TokenManager { return this.#setChainRateLimiterConfigs.execute(this.chain, opts) } + /** + * Builds an unsigned pool `setRateLimitAdmin` tx (for multisig / offline signing): assigns the + * role allowed to change the pool's rate limits alongside the owner. Probes the pool's on-chain + * `typeAndVersion` to resolve its interface + encoder. + * @remarks Owner-only, unlike the rate-limit *config* writes the pool also accepts from the + * current `rateLimitAdmin` — this call assigns the role itself, so admitting the incumbent + * admin would let it reassign or entrench its own privilege. When `sender` is supplied it is + * checked against the pool's `owner()` before any calldata is built; omit it and no owner read + * is made (nothing to compare against). + * + * A zero `newRateLimitAdmin` is accepted and clears the delegation, leaving the owner as the + * only account that can change rate limits. + * @throws {@link CCTOperationUnsupportedError} on a **v2.0.0** pool — 2.0.0 removed the + * standalone `setRateLimitAdmin(address)` selector and folded the role into a three-field + * dynamic config; use {@link generateUnsignedSetDynamicConfig} / {@link setDynamicConfig} there + * @throws {@link CCTParamsInvalidError} if any param is invalid, `poolAddress` is the zero + * address, or `sender` is given and is not the pool owner + * @throws {@link CCTContractVersionUnsupportedError} if the pool reports an unknown version + * @example + * ```typescript + * // build only — sign later (multisig / offline). `sender` must be the pool owner. + * const unsigned = await cct.generateUnsignedSetRateLimitAdmin({ + * poolAddress: '0xPool...', + * newRateLimitAdmin: '0xOpsMultisig...', + * sender: '0xOwner...', + * }) + * ``` + */ + generateUnsignedSetRateLimitAdmin(opts: SetRateLimitAdminParams): Promise { + return this.#setRateLimitAdmin.generate(this.chain, opts) + } + + /** + * Assigns the pool's rate-limit admin role, signing + submitting with `opts.wallet`. `sender` + * defaults to the wallet's address and must equal it — the wallet must be the pool owner. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTOperationUnsupportedError} on a v2.0.0 pool — use {@link setDynamicConfig} + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the wallet's address, or the wallet is not the pool owner + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.setRateLimitAdmin({ + * poolAddress: '0xPool...', + * newRateLimitAdmin: '0xOpsMultisig...', + * wallet, + * }) + * ``` + */ + setRateLimitAdmin(opts: EVMExecuteParams): Promise { + return this.#setRateLimitAdmin.execute(this.chain, opts) + } + + /** + * Builds an unsigned pool `setDynamicConfig` tx (for multisig / offline signing): replaces a + * **v2.0.0** pool's whole dynamic config — the `router` it accepts ramp calls from, plus the + * `rateLimitAdmin` and `feeAdmin` delegate roles. + * @remarks This is where the pre-2.0.0 `setRouter` / `setRateLimitAdmin` setters went: 2.0.0 + * removed them and writes all three fields together. Consequently **all three params are + * required** — this op deliberately does *not* read `getDynamicConfig()` to fill in what the + * caller omitted. The calldata has to be deterministic at build time: a multisig or cold wallet + * may sign it days later, and a hidden read would bake a value that has since moved on-chain, + * silently reverting an unrelated config change made in the interim. + * + * Read the current triple with {@link getTokenPoolState} and pass it back explicitly, so what + * is signed is exactly what was reviewed. This is also the migration path off + * {@link setRateLimitAdmin} for a 2.0.0 pool. + * + * Owner-only, for the same escalation reason as {@link generateUnsignedSetRateLimitAdmin}. + * Zero `rateLimitAdmin` / `feeAdmin` clear those delegations; `router` must be non-zero, since + * a zero router detaches the pool from CCIP rather than clearing a privilege. + * @throws {@link CCTOperationUnsupportedError} on a pre-v2.0.0 pool, which has no + * `setDynamicConfig` — use {@link generateUnsignedSetRateLimitAdmin} there + * @throws {@link CCTParamsInvalidError} if any param is invalid, `poolAddress` or `router` is + * the zero address, or `sender` is given and is not the pool owner + * @throws {@link CCTContractVersionUnsupportedError} if the pool reports an unknown version + * @example + * ```typescript + * // build only — sign later (multisig / offline). `sender` must be the pool owner. + * const unsigned = await cct.generateUnsignedSetDynamicConfig({ + * poolAddress: '0xPool...', + * router: '0xRouter...', + * rateLimitAdmin: '0xOpsMultisig...', + * feeAdmin: '0xFeeMultisig...', + * sender: '0xOwner...', + * }) + * ``` + */ + generateUnsignedSetDynamicConfig(opts: SetDynamicConfigParams): Promise { + return this.#setDynamicConfig.generate(this.chain, opts) + } + + /** + * Replaces a v2.0.0 pool's dynamic config, signing + submitting with `opts.wallet`. `sender` + * defaults to the wallet's address and must equal it — the wallet must be the pool owner. + * @remarks Replaces the config wholesale, so **all three params are required** — this op + * deliberately does *not* read `getDynamicConfig()` to fill in what the caller omitted, so an + * omitted field is reset rather than left alone. Read the current triple with + * {@link getTokenPoolState} and pass back the fields you are not changing, so what is submitted + * is exactly what was reviewed. + * + * Zero `rateLimitAdmin` / `feeAdmin` clear those delegations; `router` must be non-zero, since + * a zero router detaches the pool from CCIP rather than clearing a privilege. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTOperationUnsupportedError} on a pre-v2.0.0 pool — use {@link setRateLimitAdmin} + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the wallet's address, or the wallet is not the pool owner + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.setDynamicConfig({ + * poolAddress: '0xPool...', + * router: '0xRouter...', + * rateLimitAdmin: '0xOpsMultisig...', + * feeAdmin: '0xFeeMultisig...', + * wallet, + * }) + * ``` + */ + setDynamicConfig(opts: EVMExecuteParams): Promise { + return this.#setDynamicConfig.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 — diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/set-dynamic-config.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/set-dynamic-config.test.ts new file mode 100644 index 000000000..ead339ca8 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/set-dynamic-config.test.ts @@ -0,0 +1,284 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { parseTypeAndVersion } from '../../../../utils.ts' +import { CCTOperationUnsupportedError, CCTParamsInvalidError } from '../../../errors.ts' +import { type TokenPoolFamily, TOKEN_POOL_INTERFACES, TokenPoolVersion } from '../contracts.ts' +import { type SetDynamicConfigParams, SetDynamicConfig } from './set-dynamic-config.ts' + +const POOL = '0x' + '11'.repeat(20) +const OWNER = '0x' + '22'.repeat(20) +const RATE_LIMIT_ADMIN = '0x' + '33'.repeat(20) +const FEE_ADMIN = '0x' + '44'.repeat(20) +const ROUTER = '0x' + '55'.repeat(20) +const NOT_THE_OWNER = '0x' + '88'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +/** + * Byte-parity oracle: a fresh Interface built from the signature literal, so the assertion is + * independent of the SDK's cached, ABI-derived interfaces. + */ +const IFACE = new Interface([ + 'function setDynamicConfig(address router, address rateLimitAdmin, address feeAdmin)', +]) +const dataFor = (router: string, rateLimitAdmin: string, feeAdmin: string) => + IFACE.encodeFunctionData('setDynamicConfig', [router, rateLimitAdmin, feeAdmin]) + +const DATA = dataFor(ROUTER, RATE_LIMIT_ADMIN, FEE_ADMIN) + +/** Pool type reported by `typeAndVersion` for each ABI family. */ +const POOL_TYPE: Record = { + BurnMint: 'BurnMintTokenPool', + LockRelease: 'LockReleaseTokenPool', +} + +/** + * EVMChain stub: `typeAndVersion` reports the requested family/version, and `provider.call` + * answers `owner()` — the only read this op makes. Any other selector reverts, which is what + * pins "no hidden `getDynamicConfig()` read". + */ +function stubChain({ + family = 'BurnMint', + version = TokenPoolVersion.V2_0_0, + owner = OWNER, + onCall, +}: { + family?: TokenPoolFamily + version?: TokenPoolVersion + owner?: string + onCall?: (selector?: string) => void +} = {}): EVMChain { + const iface = TOKEN_POOL_INTERFACES[family][version] + return { + provider: { + call: async ({ data }: { data: string }) => { + onCall?.(data.slice(0, 10)) + if (data.slice(0, 10) !== iface.getFunction('owner')!.selector) + throw makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: null, data }, + invocation: null, + revert: null, + }) + return iface.encodeFunctionResult('owner', [owner]) + }, + }, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: () => { + onCall?.() + return Promise.resolve(parseTypeAndVersion(`${POOL_TYPE[family]} ${version}`)) + }, + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(address = OWNER, waitError?: Error) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + 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 SetDynamicConfig() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { + poolAddress: POOL, + router: ROUTER, + rateLimitAdmin: RATE_LIMIT_ADMIN, + feeAdmin: FEE_ADMIN, + sender: OWNER, + ...overrides, + }) +} + +/** Versions with no `setDynamicConfig` — the struct write landed in 2.0.0. */ +const UNSUPPORTED = [ + TokenPoolVersion.V1_5_0, + TokenPoolVersion.V1_5_1, + TokenPoolVersion.V1_6_1, +] as const + +describe('SetDynamicConfig (cct/evm)', () => { + describe('generate', () => { + for (const family of ['BurnMint', 'LockRelease'] as const) { + it(`encodes setDynamicConfig(router, rateLimitAdmin, feeAdmin) for a ${family} 2.0.0 pool`, async () => { + const unsigned = await generate(stubChain({ family })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, POOL) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, DATA) + }) + } + + it('emits identical calldata for both ABI families', async () => { + const [burnMint, lockRelease] = await Promise.all([ + generate(stubChain({ family: 'BurnMint' })), + generate(stubChain({ family: 'LockRelease' })), + ]) + assert.equal(burnMint.transactions[0]!.data, lockRelease.transactions[0]!.data) + }) + + it('allows the zero address to clear either delegate role', async () => { + const unsigned = await generate(stubChain(), { + rateLimitAdmin: ZeroAddress, + feeAdmin: ZeroAddress, + }) + assert.equal(unsigned.transactions[0]!.data, dataFor(ROUTER, ZeroAddress, ZeroAddress)) + }) + + it('omits from — and skips the owner read — when sender is not supplied', async () => { + let calls = 0 + const unsigned = await generate(stubChain({ onCall: () => (calls += 1) }), { + sender: undefined, + }) + assert.equal(unsigned.transactions[0]!.from, undefined) + // typeAndVersion only; no owner() round trip + assert.equal(calls, 1) + }) + + // the TOCTOU guard: all three fields come from the caller, so nothing is read back and + // baked into the calldata between build time and (possibly much later) signing. + it('never reads getDynamicConfig — the only call made is owner()', async () => { + const iface = TOKEN_POOL_INTERFACES.BurnMint[TokenPoolVersion.V2_0_0] + const selectors: (string | undefined)[] = [] + await generate(stubChain({ onCall: (selector) => selectors.push(selector) })) + assert.deepEqual(selectors, [undefined, iface.getFunction('owner')!.selector]) + assert.ok(!selectors.includes(iface.getFunction('getDynamicConfig')!.selector)) + }) + }) + + describe('validation', () => { + for (const [param, value] of [ + ['poolAddress', 'not-an-address'], + ['poolAddress', ZeroAddress], + ['router', 'not-an-address'], + ['router', ZeroAddress], + ['rateLimitAdmin', 'not-an-address'], + ['feeAdmin', 'not-an-address'], + ['sender', 'not-an-address'], + ] as const) { + it(`rejects ${param} = ${value} before any RPC`, async () => { + let called = false + await assert.rejects( + () => generate(stubChain({ onCall: () => (called = true) }), { [param]: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setDynamicConfig' && + err.context.param === param, + ) + assert.equal(called, false) + }) + } + }) + + describe('version dispatch', () => { + it('supports 2.0.0', async () => { + const unsigned = await generate(stubChain({ version: TokenPoolVersion.V2_0_0 })) + assert.equal(unsigned.transactions[0]!.data, DATA) + }) + + // no `null` ceiling is registered below 2.0.0: floor-match walks downwards and finds + // nothing at or below these versions, so they are unsupported for free. + for (const version of UNSUPPORTED) { + it(`rejects ${version} — setDynamicConfig landed in 2.0.0`, async () => { + await assert.rejects( + () => generate(stubChain({ version })), + (err: unknown) => + err instanceof CCTOperationUnsupportedError && + err.context.operation === 'setDynamicConfig' && + err.context.version === version, + ) + }) + } + + it('reports a pre-2.0.0 LockRelease pool unsupported too', async () => { + await assert.rejects( + () => generate(stubChain({ family: 'LockRelease', version: TokenPoolVersion.V1_6_1 })), + CCTOperationUnsupportedError, + ) + }) + }) + + describe('pre-transaction validation', () => { + it('rejects a sender that is not the pool owner', async () => { + await assert.rejects( + () => generate(stubChain({ owner: NOT_THE_OWNER })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setDynamicConfig' && + err.context.param === 'sender', + ) + }) + }) + + describe('execute', () => { + const params = { + poolAddress: POOL, + router: ROUTER, + rateLimitAdmin: RATE_LIMIT_ADMIN, + feeAdmin: FEE_ADMIN, + } + + it('signs and submits as the owner, resolving to the tx hash', async () => { + assert.deepEqual(await op.execute(stubChain(), { ...params, wallet: fakeSigner() }), { + hash: HASH, + }) + }) + + it('maps an on-chain revert to CCIPExecTxRevertedError', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + ...params, + wallet: fakeSigner(OWNER, makeError('execution reverted', 'CALL_EXCEPTION')), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && err.context.operation === 'setDynamicConfig', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, wallet: {} }), + CCIPWalletInvalidError, + ) + }) + + it('rejects a sender that is not the executing wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, sender: FEE_ADMIN, wallet: fakeSigner() }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a wallet that is not the pool owner', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, wallet: fakeSigner(FEE_ADMIN) }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setDynamicConfig' && + err.context.param === 'sender' && + // names the owner it read, so the caller can see which address it needed + err.message.includes(OWNER), + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/set-dynamic-config.ts b/ccip-sdk/src/cct/evm/token-pool/operations/set-dynamic-config.ts new file mode 100644 index 000000000..b7393d42a --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/set-dynamic-config.ts @@ -0,0 +1,163 @@ +/** + * setDynamicConfig — writes a v2.0.0 TokenPool's whole dynamic config in one call: the `router` + * it accepts ramp calls from, plus the `rateLimitAdmin` and `feeAdmin` delegate roles. + * + * @remarks **v2.0.0 only.** This is where the standalone pre-2.0.0 setters went: `setRouter` and + * `setRateLimitAdmin` were removed and the three fields folded into one struct written together. + * On a 1.5.0/1.5.1/1.6.1 pool the encoder table matches nothing at or below the resolved version, + * so the op reports itself unsupported — use `setRateLimitAdmin` there. + * + * **This op does not read `getDynamicConfig()` to fill in fields the caller left out, and all + * three are therefore required.** `generateUnsignedSetDynamicConfig` has to produce deterministic + * calldata: a multisig or cold wallet may sign it days after it was built, and a hidden read at + * build time would open a TOCTOU window in which the "current" value baked into the calldata has + * since changed on-chain — silently reverting an unrelated config change made in the interim. + * Callers read the current triple with `getTokenPoolState` (which on a 2.0.0 pool already returns + * `router`, `rateLimitAdmin` and `feeAdmin`, sourced from `getDynamicConfig()`) and pass all three + * back explicitly, so what is signed is exactly what was reviewed. + * + * Owner-only, deliberately: the pool accepts rate-limit *config* writes from the current + * `rateLimitAdmin` as well as the owner, but this call assigns that role, so admitting the + * `rateLimitAdmin` as `sender` would let it reassign or entrench its own privilege. + * + * @packageDocumentation + */ + +import type { Interface } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import type { TransactionResult } from '../../../operation.ts' +import { type EVMExecuteParams, EVMOperation, callTx } from '../../operation.ts' +import { validateAddress, validateNonZeroAddress } from '../../validate.ts' +import { + TokenPoolVersion, + assertPoolOwner, + getTokenPoolInterface, + resolveEncoder, + resolveTokenPool, +} from '../contracts.ts' + +/** + * Parameters for {@link SetDynamicConfig}. The three config fields keep the contract struct's + * names — `router`/`rateLimitAdmin`/`feeAdmin`, with no `new` prefix — because this op replaces + * the whole struct rather than assigning one role, and every field is **required**: omitting one + * would mean reading the current value at build time, which is exactly what this op refuses to do + * (see the module remarks). + */ +export type SetDynamicConfigParams = { + /** Token pool to reconfigure. Must be non-zero — it is the tx `to`, and a call to `0x0` hits no + * code, so it would mine as a successful no-op. */ + poolAddress: string + /** + * Router the pool accepts `lockOrBurn`/`releaseOrMint` calls from. Must be non-zero: unlike the + * two admin roles this is not a delegable privilege but the pool's only bridging counterparty, + * so a zero value does not "clear" anything — it detaches the pool from CCIP entirely and every + * transfer through it reverts until an owner tx restores it. + */ + router: string + /** + * Address allowed to change the pool's rate limits alongside the owner. The zero address is + * **allowed** and meaningful: it clears the delegation, leaving the owner as the only account + * that can change rate limits. Revoking a delegated admin is legitimate — and on incident + * response, urgent — so it is not rejected here. + */ + rateLimitAdmin: string + /** + * Address allowed to change the pool's token-transfer fee config alongside the owner. Zero is + * **allowed**, on the same reasoning as {@link SetDynamicConfigParams.rateLimitAdmin}. + */ + feeAdmin: string + /** + * The pool owner. Sets `tx.from` for offline / multisig signing, and when supplied is checked + * against the pool's on-chain `owner()` before any calldata is built. Optional for + * {@link SetDynamicConfig.generate} (an offline builder may not yet know the signer); + * {@link SetDynamicConfig.execute} defaults it to the signing wallet, so the owner check always + * runs on a broadcast tx. + */ + sender?: string +} + +/** Encodes `setDynamicConfig` calldata against the resolved pool {@link Interface}. */ +type Encoder = (iface: Interface, params: SetDynamicConfigParams) => UnsignedEVMTx + +const encodeSetDynamicConfig: Encoder = ( + iface, + { poolAddress, router, rateLimitAdmin, feeAdmin }, +) => + callTx( + poolAddress, + iface.encodeFunctionData('setDynamicConfig', [router, rateLimitAdmin, feeAdmin]), + ) + +/** Replaces a v2.0.0 TokenPool's dynamic config (`router`, `rateLimitAdmin`, `feeAdmin`). */ +export class SetDynamicConfig extends EVMOperation { + readonly name = 'setDynamicConfig' + + /** + * v2.0.0 only, and no `null` ceiling is needed for the versions below it: floor-match walks + * *downwards* from the resolved version, so 1.5.0/1.5.1/1.6.1 find nothing at or below + * themselves and are reported unsupported for free. + */ + private readonly encoders: Partial> = { + [TokenPoolVersion.V2_0_0]: encodeSetDynamicConfig, + } + + /** Validates all four addresses before any RPC; only `router` and `poolAddress` must be non-zero. */ + protected override validate({ + poolAddress, + router, + rateLimitAdmin, + feeAdmin, + }: SetDynamicConfigParams): void { + validateNonZeroAddress(this.name, 'poolAddress', poolAddress) + validateNonZeroAddress(this.name, 'router', router) + validateAddress(this.name, 'rateLimitAdmin', rateLimitAdmin) + validateAddress(this.name, 'feeAdmin', feeAdmin) + } + + /** + * Resolves the pool's type/version, confirms `sender` (when given) is the pool owner, then + * floor-matches the encoder against that version. No `getDynamicConfig()` read — see the + * module remarks. + * @remarks The owner check lives here, not only in {@link execute}, so the offline / multisig + * path gets it too: `generateUnsignedSetDynamicConfig` with an unauthorized `sender` would + * otherwise hand back a fully-formed transaction that reverts `Unauthorized` only after being + * reviewed and signed. Every sibling owner-gated pool write gates in `buildUnsigned` for the + * same reason. + * @remarks Ordered *after* the encoder so a pre-2.0.0 pool reports the real problem (no such + * function) rather than spending a round trip and failing on an authorization detail. + * @throws {@link CCTOperationUnsupportedError} on a pre-v2.0.0 pool, which has no + * `setDynamicConfig` + * @throws {@link CCTParamsInvalidError} if `sender` is given and is not the pool owner + */ + protected async buildUnsigned( + chain: EVMChain, + params: SetDynamicConfigParams, + ): Promise { + const { type, version } = await resolveTokenPool(chain, params.poolAddress) + const encode = resolveEncoder(this.encoders, version, this.name) + const unsigned = encode(getTokenPoolInterface(type, version), params) + if (params.sender !== undefined) + await assertPoolOwner(this.name, chain, params.poolAddress, params.sender) + return unsigned + } + + /** + * Signs and submits as the pool owner, defaulting `sender` to the signing wallet — the only + * address that can satisfy {@link buildUnsigned}'s owner check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} 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 is not the pool owner + * @throws {@link CCTOperationUnsupportedError} on a pre-v2.0.0 pool + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/set-rate-limit-admin.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/set-rate-limit-admin.test.ts new file mode 100644 index 000000000..62431fd52 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/set-rate-limit-admin.test.ts @@ -0,0 +1,254 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { parseTypeAndVersion } from '../../../../utils.ts' +import { CCTOperationUnsupportedError, CCTParamsInvalidError } from '../../../errors.ts' +import { type TokenPoolFamily, TOKEN_POOL_INTERFACES, TokenPoolVersion } from '../contracts.ts' +import { type SetRateLimitAdminParams, SetRateLimitAdmin } from './set-rate-limit-admin.ts' + +const POOL = '0x' + '11'.repeat(20) +const OWNER = '0x' + '22'.repeat(20) +const NEW_ADMIN = '0x' + '44'.repeat(20) +const NOT_THE_OWNER = '0x' + '88'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +/** + * Byte-parity oracle: a fresh Interface built from the signature literal, so the assertion is + * independent of the SDK's cached, ABI-derived interfaces. + */ +const IFACE = new Interface(['function setRateLimitAdmin(address rateLimitAdmin)']) +const dataFor = (admin: string) => IFACE.encodeFunctionData('setRateLimitAdmin', [admin]) + +/** Pool type reported by `typeAndVersion` for each ABI family. */ +const POOL_TYPE: Record = { + BurnMint: 'BurnMintTokenPool', + LockRelease: 'LockReleaseTokenPool', +} + +/** + * EVMChain stub: `typeAndVersion` reports the requested family/version, and `provider.call` + * answers `owner()` (the only read this op makes) off the pool's own Interface. Every other + * selector reverts, which is what pins "no other RPC". + */ +function stubChain({ + family = 'BurnMint', + version = TokenPoolVersion.V1_5_0, + owner = OWNER, + onCall, +}: { + family?: TokenPoolFamily + version?: TokenPoolVersion + owner?: string + onCall?: () => void +} = {}): EVMChain { + const iface = TOKEN_POOL_INTERFACES[family][version] + return { + provider: { + call: async ({ data }: { data: string }) => { + onCall?.() + if (data.slice(0, 10) !== iface.getFunction('owner')!.selector) + throw makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: null, data }, + invocation: null, + revert: null, + }) + return iface.encodeFunctionResult('owner', [owner]) + }, + }, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: () => { + onCall?.() + return Promise.resolve(parseTypeAndVersion(`${POOL_TYPE[family]} ${version}`)) + }, + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(address = OWNER, waitError?: Error) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + 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 SetRateLimitAdmin() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { + poolAddress: POOL, + newRateLimitAdmin: NEW_ADMIN, + sender: OWNER, + ...overrides, + }) +} + +/** Versions that still declare `setRateLimitAdmin`; 2.0.0 removed it. */ +const SUPPORTED = [ + TokenPoolVersion.V1_5_0, + TokenPoolVersion.V1_5_1, + TokenPoolVersion.V1_6_1, +] as const + +describe('SetRateLimitAdmin (cct/evm)', () => { + describe('generate', () => { + for (const version of SUPPORTED) { + for (const family of ['BurnMint', 'LockRelease'] as const) { + it(`encodes setRateLimitAdmin(admin) for a ${family} ${version} pool`, async () => { + const unsigned = await generate(stubChain({ family, version })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, POOL) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, dataFor(NEW_ADMIN)) + }) + } + + it(`emits identical calldata for both ABI families at ${version}`, async () => { + const [burnMint, lockRelease] = await Promise.all([ + generate(stubChain({ family: 'BurnMint', version })), + generate(stubChain({ family: 'LockRelease', version })), + ]) + assert.equal(burnMint.transactions[0]!.data, lockRelease.transactions[0]!.data) + }) + } + + it('allows the zero address to clear the rate-limit admin role', async () => { + const unsigned = await generate(stubChain(), { newRateLimitAdmin: ZeroAddress }) + assert.equal(unsigned.transactions[0]!.data, dataFor(ZeroAddress)) + }) + + it('omits from — and skips the owner read — when sender is not supplied', async () => { + let calls = 0 + const unsigned = await generate(stubChain({ onCall: () => (calls += 1) }), { + sender: undefined, + }) + assert.equal(unsigned.transactions[0]!.from, undefined) + // typeAndVersion only; no owner() round trip + assert.equal(calls, 1) + }) + }) + + describe('validation', () => { + for (const [param, value] of [ + ['poolAddress', 'not-an-address'], + ['poolAddress', ZeroAddress], + ['newRateLimitAdmin', 'not-an-address'], + ['sender', 'not-an-address'], + ] as const) { + it(`rejects ${param} = ${value} before any RPC`, async () => { + let called = false + await assert.rejects( + () => generate(stubChain({ onCall: () => (called = true) }), { [param]: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setRateLimitAdmin' && + err.context.param === param, + ) + assert.equal(called, false) + }) + } + }) + + describe('version dispatch', () => { + for (const version of SUPPORTED) { + it(`supports ${version}`, async () => { + const unsigned = await generate(stubChain({ version })) + assert.equal(unsigned.transactions[0]!.data, dataFor(NEW_ADMIN)) + }) + } + + it('rejects a 2.0.0 pool — the selector was removed, use setDynamicConfig', async () => { + await assert.rejects( + () => generate(stubChain({ version: TokenPoolVersion.V2_0_0 })), + (err: unknown) => + err instanceof CCTOperationUnsupportedError && + err.context.operation === 'setRateLimitAdmin' && + err.context.version === TokenPoolVersion.V2_0_0, + ) + }) + + it('reports 2.0.0 unsupported for the LockRelease family too', async () => { + await assert.rejects( + () => generate(stubChain({ family: 'LockRelease', version: TokenPoolVersion.V2_0_0 })), + CCTOperationUnsupportedError, + ) + }) + }) + + describe('pre-transaction validation', () => { + it('rejects a sender that is not the pool owner', async () => { + await assert.rejects( + () => generate(stubChain({ owner: NOT_THE_OWNER })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setRateLimitAdmin' && + err.context.param === 'sender', + ) + }) + }) + + describe('execute', () => { + const params = { poolAddress: POOL, newRateLimitAdmin: NEW_ADMIN } + + it('signs and submits as the owner, resolving to the tx hash', async () => { + assert.deepEqual(await op.execute(stubChain(), { ...params, wallet: fakeSigner() }), { + hash: HASH, + }) + }) + + it('maps an on-chain revert to CCIPExecTxRevertedError', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + ...params, + wallet: fakeSigner(OWNER, makeError('execution reverted', 'CALL_EXCEPTION')), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && err.context.operation === 'setRateLimitAdmin', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, wallet: {} }), + CCIPWalletInvalidError, + ) + }) + + it('rejects a sender that is not the executing wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, sender: NEW_ADMIN, wallet: fakeSigner() }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a wallet that is not the pool owner', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, wallet: fakeSigner(NEW_ADMIN) }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'setRateLimitAdmin' && + err.context.param === 'sender' && + // names the owner it read, so the caller can see which address it needed + err.message.includes(OWNER), + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/set-rate-limit-admin.ts b/ccip-sdk/src/cct/evm/token-pool/operations/set-rate-limit-admin.ts new file mode 100644 index 000000000..dcc18bb3d --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/set-rate-limit-admin.ts @@ -0,0 +1,132 @@ +/** + * setRateLimitAdmin — assigns the TokenPool role allowed to change rate limits alongside the + * owner (v1.5.0–v1.6.1 only). + * + * @remarks **Removed in v2.0.0.** The standalone `setRateLimitAdmin(address)` selector does not + * exist on a 2.0.0 pool: the role was folded into a three-field dynamic config + * (`router`/`rateLimitAdmin`/`feeAdmin`) written in one shot by `setDynamicConfig`. The encoder + * table therefore pins an explicit `null` ceiling at 2.0.0 so a 2.0.0 pool is reported + * unsupported instead of floor-matching the 1.5.0 encoder and emitting calldata for a selector + * the pool does not implement. Use {@link SetDynamicConfig} there. + * + * Owner-only, deliberately: unlike the rate-limit *config* ops — which the pool accepts from + * either the owner or the current `rateLimitAdmin` — this op assigns the role itself, so + * accepting the `rateLimitAdmin` as `sender` would let it reassign (or entrench) its own + * privilege. Only the pool `owner` is allowed through. + * + * @packageDocumentation + */ + +import type { Interface } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import type { UnsignedEVMTx } from '../../../../evm/types.ts' +import type { TransactionResult } from '../../../operation.ts' +import { type EVMExecuteParams, EVMOperation, callTx } from '../../operation.ts' +import { validateAddress, validateNonZeroAddress } from '../../validate.ts' +import { + TokenPoolVersion, + assertPoolOwner, + getTokenPoolInterface, + resolveEncoder, + resolveTokenPool, +} from '../contracts.ts' + +/** Parameters for {@link SetRateLimitAdmin}. */ +export type SetRateLimitAdminParams = { + /** Token pool whose rate-limit admin role is being assigned. Must be non-zero — it is the tx + * `to`, and a call to `0x0` hits no code, so it would mine as a successful no-op. */ + poolAddress: string + /** + * Address to grant the rate-limit admin role to. Named `newRateLimitAdmin` to match the Solana + * op's public field (`cct/solana/token-pool/operations/set-rate-limit-admin.ts`) rather than the + * ABI's bare `rateLimitAdmin`, so cross-family callers write one shape. + * + * The zero address is **allowed** and meaningful: it clears the role, leaving the owner as the + * only account that can change rate limits. Revoking a delegated admin is a legitimate — and + * on incident response, urgent — operation, so it is not rejected here. + */ + newRateLimitAdmin: string + /** + * The pool owner. Sets `tx.from` for offline / multisig signing, and when supplied is checked + * against the pool's on-chain `owner()` before any calldata is built. Optional for + * {@link SetRateLimitAdmin.generate} (an offline builder may not yet know the signer); + * {@link SetRateLimitAdmin.execute} defaults it to the signing wallet, so the owner check + * always runs on a broadcast tx. + */ + sender?: string +} + +/** Encodes `setRateLimitAdmin` calldata against the resolved pool {@link Interface}. */ +type Encoder = (iface: Interface, params: SetRateLimitAdminParams) => UnsignedEVMTx + +const encodeSetRateLimitAdmin: Encoder = (iface, { poolAddress, newRateLimitAdmin }) => + callTx(poolAddress, iface.encodeFunctionData('setRateLimitAdmin', [newRateLimitAdmin])) + +/** + * Assigns a TokenPool's rate-limit admin role (v1.5.0–v1.6.1). Owner-only; removed in v2.0.0 in + * favour of {@link SetDynamicConfig}. + */ +export class SetRateLimitAdmin extends EVMOperation { + readonly name = 'setRateLimitAdmin' + + /** + * One 1.5.0 entry covers 1.5.1 and 1.6.1 by floor-match (the encoding never changed), and the + * explicit `null` at 2.0.0 is load-bearing, not decoration: without it a 2.0.0 pool would + * floor-match the 1.5.0 encoder and produce calldata for a selector that version removed. + */ + private readonly encoders: Partial> = { + [TokenPoolVersion.V1_5_0]: encodeSetRateLimitAdmin, + [TokenPoolVersion.V2_0_0]: null, + } + + /** Validates both addresses before any RPC; a zero `newRateLimitAdmin` clears the role. */ + protected override validate({ poolAddress, newRateLimitAdmin }: SetRateLimitAdminParams): void { + validateNonZeroAddress(this.name, 'poolAddress', poolAddress) + validateAddress(this.name, 'newRateLimitAdmin', newRateLimitAdmin) + } + + /** + * Resolves the pool's type/version, confirms `sender` (when given) is the pool owner, then + * floor-matches the encoder against that version. + * @remarks The owner check lives here, not only in {@link execute}, so the offline / multisig + * path gets it too: `generateUnsignedSetRateLimitAdmin` with an unauthorized `sender` would + * otherwise hand back a fully-formed transaction that reverts `Unauthorized` only after being + * reviewed and signed. Every sibling owner-gated pool write gates in `buildUnsigned` for the + * same reason. + * @remarks Ordered *after* the encoder so a 2.0.0 pool reports the real problem (removed + * selector) rather than spending a round trip and failing on an authorization detail. + * @throws {@link CCTOperationUnsupportedError} on a v2.0.0 pool — the selector was removed; + * use {@link SetDynamicConfig} + * @throws {@link CCTParamsInvalidError} if `sender` is given and is not the pool owner + */ + protected async buildUnsigned( + chain: EVMChain, + params: SetRateLimitAdminParams, + ): Promise { + const { type, version } = await resolveTokenPool(chain, params.poolAddress) + const encode = resolveEncoder(this.encoders, version, this.name) + const unsigned = encode(getTokenPoolInterface(type, version), params) + if (params.sender !== undefined) + await assertPoolOwner(this.name, chain, params.poolAddress, params.sender) + return unsigned + } + + /** + * Signs and submits as the pool owner, defaulting `sender` to the signing wallet — the only + * address that can satisfy {@link buildUnsigned}'s owner check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} 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 is not the pool owner + * @throws {@link CCTOperationUnsupportedError} on a v2.0.0 pool + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} From f468661655f6eb6c4fff924f82f32d201770bff3 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:18:46 +0100 Subject: [PATCH 78/87] feat(cct-sdk): Add apply allowlist updates evm op (#396) * feat(cct-sdk): Add get token pool remotes evm query * feat(cct-sdk): Add generic EVM param validators * feat(cct-sdk): Read siloed lock/release pool state * feat(cct-sdk): Add pool owner pre-flight and encoder removal ceilings * refactor(cct-sdk): Hoist validate/parse lifecycle to the shared Operation base * feat(cct-sdk): Add apply chain updates evm op * fix(cct-sdk): Reject a declared version that is not the pool's calldata shape * feat(cct-sdk): Add EVM remote pool ops * Address PR comments * feat(cct-sdk): Add set chain rate limiter configs op * feat(cct-sdk): Add set rate limit admin and set dynamic config ops * feat(cct-sdk): Add apply allowlist updates evm op * Fix lint * Fix lint * Fix lint * Fix lint * Fix lint * Fix tsdoc and lint * cleanup * Address PR comments * linting --- ccip-sdk/src/cct/evm/index.ts | 82 ++++ ccip-sdk/src/cct/evm/token-pool/contracts.ts | 42 +- .../apply-allowlist-updates.test.ts | 408 ++++++++++++++++++ .../operations/apply-allowlist-updates.ts | 261 +++++++++++ 4 files changed, 792 insertions(+), 1 deletion(-) create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/apply-allowlist-updates.test.ts create mode 100644 ccip-sdk/src/cct/evm/token-pool/operations/apply-allowlist-updates.ts diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 2276cf019..786b85cc5 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -44,6 +44,10 @@ import { TransferAdmin, } from './token-admin-registry/operations/transfer-admin.ts' import { type AddRemotePoolParams, AddRemotePool } from './token-pool/operations/add-remote-pool.ts' +import { + type ApplyAllowlistUpdatesParams, + ApplyAllowlistUpdates, +} from './token-pool/operations/apply-allowlist-updates.ts' import { type ApplyChainUpdatesParams, ApplyChainUpdates, @@ -108,6 +112,7 @@ export class EVMTokenManager extends TokenManager { readonly #addRemotePool = new AddRemotePool() readonly #removeRemotePool = new RemoveRemotePool() readonly #applyChainUpdates = new ApplyChainUpdates() + readonly #applyAllowlistUpdates = new ApplyAllowlistUpdates() readonly #setChainRateLimiterConfigs = new SetChainRateLimiterConfigs() readonly #setRateLimitAdmin = new SetRateLimitAdmin() readonly #setDynamicConfig = new SetDynamicConfig() @@ -1186,6 +1191,82 @@ export class EVMTokenManager extends TokenManager { generateUnsignedApplyChainUpdates(opts: ApplyChainUpdatesParams): Promise { return this.#applyChainUpdates.generate(this.chain, opts) } + + /** + * Builds an unsigned pool `applyAllowlistUpdates` tx (for multisig / offline signing): removes + * and adds entries in the pool's sender allowlist in one call. Probes the pool's on-chain + * `typeAndVersion` to resolve its interface + encoder. + * @remarks **v1.5.0–v1.6.1 only.** The allowlist feature does not exist on a v2.0.0 pool, which + * declares neither `applyAllowListUpdates` nor `getAllowList`/`getAllowListEnabled`, so a 2.0.0 + * pool is reported unsupported rather than emitting calldata for a removed selector. + * + * `removes` are applied *before* `adds` on-chain. Both arrays must be non-empty in total, hold + * no duplicates and no zero address, and share no address — an address in both would end up + * allowlisted (removes run first), which no caller can reasonably have meant. + * + * The pool must have been deployed **with** an allowlist (`allowlistEnabled` is immutable, and + * the call reverts `AllowListNotEnabled` when false), and the update must actually change + * state: the current allowlist is read first, and an entry the pool would silently ignore — a + * `removes` that is not allowlisted, an `adds` that already is — is rejected here. + * + * Owner-only (`applyAllowListUpdates` is `onlyOwner`). When `sender` is supplied it is checked + * against the pool's `owner()` before any calldata is built; omit it and no owner read is made + * (nothing to compare against). + * @throws {@link CCTOperationUnsupportedError} on a **v2.0.0** pool, which has no allowlist + * @throws {@link CCTParamsInvalidError} if any param is invalid, `poolAddress` is the zero + * address, both arrays are empty, an array holds duplicates or the zero address, an address + * appears in both arrays, the pool has no allowlist enabled, a `removes` entry is not currently + * allowlisted, an `adds` entry already is, or `sender` is given and is not the pool owner + * @throws {@link CCTContractVersionUnsupportedError} if the pool reports an unknown version + * @example + * ```typescript + * // build only — sign later (multisig / offline). `sender` must be the pool owner. + * const unsigned = await cct.generateUnsignedApplyAllowlistUpdates({ + * poolAddress: '0xPool...', + * removes: ['0xRevoked...'], + * adds: ['0xNewSender...'], + * sender: '0xOwner...', + * }) + * ``` + */ + generateUnsignedApplyAllowlistUpdates(opts: ApplyAllowlistUpdatesParams): Promise { + return this.#applyAllowlistUpdates.generate(this.chain, opts) + } + + /** + * Removes and adds entries in the pool's sender allowlist, signing + submitting with + * `opts.wallet`. `sender` defaults to the wallet's address and must equal it — the wallet must + * be the pool owner. + * + * `removes` are applied *before* `adds` on-chain, so an address listed in both would end up + * allowlisted; that is rejected, as are duplicates and the zero address. The pool must have an + * allowlist enabled (`allowlistEnabled` is immutable — a pool deployed without one can never + * gain it), and every entry must change state: the current allowlist is read first, and a + * `removes` that is not allowlisted or an `adds` that already is fails here rather than mining + * as a no-op. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTOperationUnsupportedError} on a v2.0.0 pool, which has no allowlist + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the wallet's address, the wallet is not the pool owner, the pool has no allowlist enabled, or + * an entry would be a no-op (see {@link EVMTokenManager.generateUnsignedApplyAllowlistUpdates}) + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.applyAllowlistUpdates({ + * poolAddress: '0xPool...', + * removes: ['0xRevoked...'], + * adds: ['0xNewSender...'], + * wallet, + * }) + * ``` + */ + applyAllowlistUpdates( + opts: EVMExecuteParams, + ): Promise { + return this.#applyAllowlistUpdates.execute(this.chain, opts) + } } export * from '../errors.ts' @@ -1234,6 +1315,7 @@ export type { ChainUpdateV1_5_0, ChainUpdateV1_5_1, } from './token-pool/operations/apply-chain-updates.ts' +export type { ApplyAllowlistUpdatesParams } from './token-pool/operations/apply-allowlist-updates.ts' /** * `GetTokenPoolRemotesResult` is a `Record`, so a caller cannot name a * single lane's type without these. Declared in `../../chain.ts` (shared with the core diff --git a/ccip-sdk/src/cct/evm/token-pool/contracts.ts b/ccip-sdk/src/cct/evm/token-pool/contracts.ts index 1916c865c..be14ee687 100644 --- a/ccip-sdk/src/cct/evm/token-pool/contracts.ts +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.ts @@ -3,7 +3,8 @@ * resolution ({@link resolveTokenPool}, {@link getTokenPoolInterface}, floor-matched via * {@link resolveEncoder}) for read/write ops, plus the deployable pools' creation artifacts * ({@link getTokenPoolArtifact}), the narrow role reads every owner-gated write pre-flights - * `sender` against ({@link readTokenPoolOwner}, {@link readTokenPoolRateLimitAdmin}) plus the + * `sender` against ({@link readTokenPoolOwner}, {@link readTokenPoolRateLimitAdmin}), the allowlist read + * `applyAllowlistUpdates` pre-flights against ({@link readTokenPoolAllowlist}) plus the * owner-only guard built on the first of them ({@link assertPoolOwner}). The write-side * rate-limit shape lane-config ops share lives in `rate-limit.ts`. Mirrors `token/contracts.ts`. * @@ -246,6 +247,45 @@ export async function readTokenPoolOwner(chain: EVMChain, poolAddress: string): return getAddress(resultToObject(await pool.owner())) } +/** + * `TokenPool`'s allowlist getters, identical across v1.5.0–v1.6.1 and both ABI families. Absent + * from v2.0.0, which dropped the allowlist — callers must resolve the version first. + */ +type PoolAllowlistGetter = Pick< + TypedContract, + 'getAllowListEnabled' | 'getAllowList' +> + +/** + * Reads a token pool's sender allowlist and whether the feature is enabled at all, in two + * parallel `eth_call`s. + * + * @remarks Same rationale as {@link readTokenPoolOwner} for not routing through + * `getTokenPoolState`, which does not expose the allowlist. + * @remarks `enabled` is fixed for the pool's lifetime: the contract sets `i_allowlistEnabled` + * *immutable* in its constructor, to `allowlist.length > 0`. A pool deployed without an + * allowlist can therefore never gain one, and every `applyAllowListUpdates` against it reverts + * `AllowListNotEnabled`. + * @param chain - Chain to read from. + * @param poolAddress - Token pool contract to read from; must be v1.5.0–v1.6.1. + * @returns `enabled`, and the current entries checksummed (empty when disabled). + */ +export async function readTokenPoolAllowlist( + chain: EVMChain, + poolAddress: string, +): Promise<{ enabled: boolean; entries: string[] }> { + const pool: PoolAllowlistGetter = getTypedContract( + chain, + poolAddress, + BURN_MINT_TOKEN_POOL_V1_5_0_ABI, + ) + const [enabled, entries] = await Promise.all([pool.getAllowListEnabled(), pool.getAllowList()]) + return { + enabled: resultToObject(enabled), + entries: resultToObject(entries).map((entry) => getAddress(entry)), + } +} + /** * Reads a token pool's `rateLimitAdmin` — the delegated role the pools accept for rate-limit * writes alongside the owner — in a single `eth_call`. diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/apply-allowlist-updates.test.ts b/ccip-sdk/src/cct/evm/token-pool/operations/apply-allowlist-updates.test.ts new file mode 100644 index 000000000..bf63d8147 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/apply-allowlist-updates.test.ts @@ -0,0 +1,408 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, getAddress, makeError } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { parseTypeAndVersion } from '../../../../utils.ts' +import { CCTOperationUnsupportedError, CCTParamsInvalidError } from '../../../errors.ts' +import { type TokenPoolFamily, TOKEN_POOL_INTERFACES, TokenPoolVersion } from '../contracts.ts' +import { + type ApplyAllowlistUpdatesParams, + ApplyAllowlistUpdates, +} from './apply-allowlist-updates.ts' + +const POOL = '0x' + '11'.repeat(20) +const OWNER = '0x' + '22'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +// Distinct fixtures per array: a swapped (removes, adds) pair must fail byte parity. +const ADDS = ['0x' + 'a1'.repeat(20), '0x' + 'a2'.repeat(20)] +const REMOVES = ['0x' + 'e1'.repeat(20)] + +/** Independent of the SDK's cached interfaces — the reference the encoding is measured against. */ +const REFERENCE = new Interface([ + 'function applyAllowListUpdates(address[] removes, address[] adds)', +]) +const DATA = REFERENCE.encodeFunctionData('applyAllowListUpdates', [REMOVES, ADDS]) + +/** Pool types reporting each ABI family, for the `typeAndVersion` the stub answers with. */ +const POOL_TYPE: Record = { + BurnMint: 'BurnMintTokenPool', + LockRelease: 'LockReleaseTokenPool', +} + +const LEGACY_VERSIONS = [ + TokenPoolVersion.V1_5_0, + TokenPoolVersion.V1_5_1, + TokenPoolVersion.V1_6_1, +] as const + +/** + * EVMChain stub: reports `type version` from `typeAndVersion`, and answers the pool's `owner()`, + * `getAllowListEnabled()` and `getAllowList()` `eth_call`s. Any other call reverts. `onCall` + * records that RPC happened at all, so the validation tests can assert nothing was issued. + * + * `allowlist` defaults to {@link REMOVES}, the set the default params remove from — leaving + * {@link ADDS} absent, so the default case is a real state change on both sides. + */ +function stubChain({ + family = 'BurnMint', + version = TokenPoolVersion.V1_5_1, + owner = OWNER, + allowlistEnabled = true, + allowlist = REMOVES, + onCall, +}: { + family?: TokenPoolFamily + version?: TokenPoolVersion + owner?: string + allowlistEnabled?: boolean + allowlist?: string[] + onCall?: () => void +} = {}): EVMChain { + const iface = TOKEN_POOL_INTERFACES[family][version] + const ownerSelector = iface.getFunction('owner')!.selector + // v2.0.0 dropped the allowlist getters, so only look them up where they exist + const allowlistEnabledSelector = iface.getFunction('getAllowListEnabled')?.selector + const allowlistSelector = iface.getFunction('getAllowList')?.selector + return { + provider: { + call: ({ data }: { data: string }) => { + onCall?.() + const selector = data.slice(0, 10) + if (selector === ownerSelector) + return Promise.resolve(iface.encodeFunctionResult('owner', [owner])) + if (selector === allowlistEnabledSelector) + return Promise.resolve( + iface.encodeFunctionResult('getAllowListEnabled', [allowlistEnabled]), + ) + if (selector === allowlistSelector) + return Promise.resolve(iface.encodeFunctionResult('getAllowList', [allowlist])) + throw makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: null, data }, + invocation: null, + revert: null, + }) + }, + }, + logger: { debug() {}, info() {}, warn() {}, error() {} }, + typeAndVersion: () => { + onCall?.() + return Promise.resolve(parseTypeAndVersion(`${POOL_TYPE[family]} ${version}`)) + }, + nextNonce: async () => 0, + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(waitError?: Error, address = OWNER) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + 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 ApplyAllowlistUpdates() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { + poolAddress: POOL, + removes: REMOVES, + adds: ADDS, + sender: OWNER, + ...overrides, + }) +} + +describe('ApplyAllowlistUpdates (cct/evm)', () => { + describe('generate', () => { + for (const version of LEGACY_VERSIONS) { + for (const family of ['BurnMint', 'LockRelease'] as const) { + it(`encodes applyAllowListUpdates(removes, adds) for a ${family} ${version} pool`, async () => { + const unsigned = await generate(stubChain({ family, version })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, POOL) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, DATA) + }) + } + + it(`produces identical calldata for both ABI families at ${version}`, async () => { + const [burnMint, lockRelease] = await Promise.all([ + generate(stubChain({ family: 'BurnMint', version })), + generate(stubChain({ family: 'LockRelease', version })), + ]) + assert.equal(burnMint.transactions[0]!.data, lockRelease.transactions[0]!.data) + }) + } + + it('omits from, and skips the owner read, when sender is not supplied', async () => { + let calls = 0 + const unsigned = await generate(stubChain({ onCall: () => calls++ }), { sender: undefined }) + assert.equal(unsigned.transactions[0]!.from, undefined) + assert.equal(unsigned.transactions[0]!.data, DATA) + // typeAndVersion + the two allowlist reads — no owner() read without a sender to compare + // it against; the allowlist pre-flight does not depend on the signer and still runs + assert.equal(calls, 3) + }) + + it('encodes an empty removes array (adds only)', async () => { + const unsigned = await generate(stubChain(), { removes: [] }) + assert.equal( + unsigned.transactions[0]!.data, + REFERENCE.encodeFunctionData('applyAllowListUpdates', [[], ADDS]), + ) + }) + + it('encodes an empty adds array (removes only)', async () => { + const unsigned = await generate(stubChain(), { adds: [] }) + assert.equal( + unsigned.transactions[0]!.data, + REFERENCE.encodeFunctionData('applyAllowListUpdates', [REMOVES, []]), + ) + }) + + it('rejects a sender that is not the pool owner', async () => { + await assert.rejects( + () => generate(stubChain(), { sender: '0x' + '99'.repeat(20) }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyAllowlistUpdates' && + err.context.param === 'sender', + ) + }) + }) + + describe('validation', () => { + const cases: { name: string; param: string; params: Partial }[] = [ + { name: 'an invalid poolAddress', param: 'poolAddress', params: { poolAddress: 'nope' } }, + // a tx to `0x0` hits no code, so it would mine as a successful no-op rather than reverting + { name: 'the zero poolAddress', param: 'poolAddress', params: { poolAddress: ZeroAddress } }, + // `.map` skips holes, so without the density guard a sparse array validated clean and the + // hole reached ethers as `undefined` + { + name: 'a hole in adds', + param: 'adds[1]', + params: { + adds: (() => { + const sparse = [ADDS[0]!] + sparse[2] = ADDS[1] ?? ZeroAddress + return sparse + })(), + }, + }, + { + name: 'a hole in removes', + param: 'removes[1]', + params: { + removes: (() => { + const sparse = [REMOVES[0]!] + sparse[2] = REMOVES[0]! + return sparse + })(), + }, + }, + { + name: 'an invalid address inside adds', + param: 'adds[1]', + params: { adds: [ADDS[0]!, 'not-an-address'] }, + }, + { + name: 'an invalid address inside removes', + param: 'removes[0]', + params: { removes: ['not-an-address'] }, + }, + { + name: 'a missing removes', + param: 'removes', + params: { removes: undefined }, + }, + { name: 'a non-array adds', param: 'adds', params: { adds: 42 as unknown as string[] } }, + { name: 'both arrays empty', param: 'adds', params: { removes: [], adds: [] } }, + { + name: 'duplicates within adds', + param: 'adds', + params: { adds: [ADDS[0]!, ADDS[0]!] }, + }, + { + name: 'duplicates within removes, differing only in case', + param: 'removes', + params: { removes: [REMOVES[0]!, getAddress(REMOVES[0]!)] }, + }, + { + name: 'an address present in both adds and removes', + param: 'adds', + params: { adds: [ADDS[0]!], removes: [ADDS[0]!] }, + }, + { name: 'an invalid sender', param: 'sender', params: { sender: 'not-an-address' } }, + // the pool `continue`s past a zero address in adds, and can therefore never hold one: + // a silent no-op on either side, so it is rejected locally rather than encoded + { + name: 'the zero address inside adds', + param: 'adds[0]', + params: { adds: [ZeroAddress] }, + }, + { + name: 'the zero address inside removes', + param: 'removes[0]', + params: { removes: [ZeroAddress] }, + }, + ] + + for (const { name, param, params } of cases) { + it(`rejects ${name} before any RPC`, async () => { + let calls = 0 + await assert.rejects( + () => generate(stubChain({ onCall: () => calls++ }), params), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyAllowlistUpdates' && + err.context.param === param, + ) + assert.equal(calls, 0) + }) + } + }) + + describe('allowlist pre-flight', () => { + it('rejects a pool deployed without an allowlist', async () => { + await assert.rejects( + () => generate(stubChain({ allowlistEnabled: false, allowlist: [] })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyAllowlistUpdates' && + err.context.param === 'poolAddress', + ) + }) + + it('rejects removing an address that is not currently allowlisted', async () => { + // the pool's EnumerableSet.remove would return false and the tx would change nothing + await assert.rejects( + () => generate(stubChain({ allowlist: [ADDS[0]!] }), { adds: [] }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyAllowlistUpdates' && + err.context.param === 'removes', + ) + }) + + it('rejects adding an address that is already allowlisted', async () => { + await assert.rejects( + () => generate(stubChain({ allowlist: [...REMOVES, ADDS[1]!] })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyAllowlistUpdates' && + err.context.param === 'adds', + ) + }) + + it('matches the on-chain allowlist case-insensitively', async () => { + const unsigned = await generate( + stubChain({ allowlist: REMOVES.map((address) => address.toLowerCase()) }), + ) + assert.equal(unsigned.transactions[0]!.data, DATA) + }) + + it('accepts an empty allowlist when the feature is enabled (adds only)', async () => { + const unsigned = await generate(stubChain({ allowlist: [] }), { removes: [] }) + assert.equal( + unsigned.transactions[0]!.data, + REFERENCE.encodeFunctionData('applyAllowListUpdates', [[], ADDS]), + ) + }) + }) + + describe('execute', () => { + const params = { poolAddress: POOL, removes: REMOVES, adds: ADDS } + + it('signs and submits, resolving to the tx hash', async () => { + assert.deepEqual(await op.execute(stubChain(), { ...params, wallet: fakeSigner() }), { + hash: HASH, + }) + }) + + it('maps an on-chain revert to CCIPExecTxRevertedError', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + ...params, + wallet: fakeSigner(makeError('execution reverted', 'CALL_EXCEPTION')), + }), + (err: unknown) => + err instanceof CCIPExecTxRevertedError && + err.context.operation === 'applyAllowlistUpdates', + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { ...params, wallet: {} }), + CCIPWalletInvalidError, + ) + }) + + it('rejects a sender that diverges from the signing wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + ...params, + sender: '0x' + '99'.repeat(20), + wallet: fakeSigner(), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a signing wallet that is not the pool owner', async () => { + const notOwner = '0x' + '99'.repeat(20) + await assert.rejects( + () => op.execute(stubChain(), { ...params, wallet: fakeSigner(undefined, notOwner) }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'applyAllowlistUpdates' && + err.context.param === 'sender', + ) + }) + }) + + describe('version dispatch', () => { + for (const version of LEGACY_VERSIONS) { + it(`supports ${version}`, async () => { + const unsigned = await generate(stubChain({ version })) + assert.equal(unsigned.transactions[0]!.data, DATA) + }) + } + + it('rejects 2.0.0, where the allowlist was removed from the contract', async () => { + await assert.rejects( + () => generate(stubChain({ version: TokenPoolVersion.V2_0_0 })), + (err: unknown) => + err instanceof CCTOperationUnsupportedError && + err.context.operation === 'applyAllowlistUpdates' && + err.context.version === TokenPoolVersion.V2_0_0, + ) + }) + + it('covers every known TokenPoolVersion', () => { + assert.deepEqual(Object.values(TokenPoolVersion), [ + ...LEGACY_VERSIONS, + TokenPoolVersion.V2_0_0, + ]) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/apply-allowlist-updates.ts b/ccip-sdk/src/cct/evm/token-pool/operations/apply-allowlist-updates.ts new file mode 100644 index 000000000..e89a5cec5 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token-pool/operations/apply-allowlist-updates.ts @@ -0,0 +1,261 @@ +/** + * applyAllowlistUpdates — replaces entries in a token pool's sender allowlist, the set of local + * addresses permitted to initiate a CCIP transfer through the pool. Removes are applied before + * adds, in one call: `applyAllowListUpdates(address[] removes, address[] adds)`. + * + * @remarks Requires the pool to have been deployed *with* an allowlist: `allowlistEnabled` is + * immutable, and the call reverts `AllowListNotEnabled` when it is false. That, and every update + * the pool would silently ignore, is pre-flighted against the current allowlist before any + * calldata is built. + * + * @remarks Available on v1.5.0–v1.6.1 with an unchanged signature, and **removed outright in + * v2.0.0**, which has no allowlist. The encoder table pins an explicit `null` ceiling at + * {@link TokenPoolVersion.V2_0_0} so {@link resolveEncoder}'s floor-match cannot inherit the + * 1.5.0 encoder upward and emit calldata for a selector the pool does not implement; a 2.0.0 pool + * reports {@link CCTOperationUnsupportedError} instead. + * + * @packageDocumentation + */ + +import { type Interface, 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 { validateArray, validateNonZeroAddress } from '../../validate.ts' +import { + TokenPoolVersion, + assertPoolOwner, + getTokenPoolInterface, + readTokenPoolAllowlist, + resolveEncoder, + resolveTokenPool, +} from '../contracts.ts' + +/** Parameters for {@link ApplyAllowlistUpdates}. */ +export type ApplyAllowlistUpdatesParams = { + /** Token pool contract address whose allowlist is being updated. */ + poolAddress: string + /** + * Addresses to remove from the allowlist. Applied *before* {@link adds} on-chain. Must contain + * no duplicates, no zero address, and no address that also appears in {@link adds}. Every entry + * must currently be allowlisted — the pool silently ignores the rest. + */ + removes: string[] + /** + * Addresses to add to the allowlist. Must contain no duplicates, no zero address, and no + * address that also appears in {@link removes}. No entry may already be allowlisted — the pool + * silently ignores the rest. + */ + adds: string[] + /** + * Current pool owner; sets `tx.from` for offline / multisig signing. When supplied it is also + * checked against the pool's on-chain `owner()` before any calldata is built, since the pool + * gates `applyAllowListUpdates` on `onlyOwner`. + */ + sender?: string +} + +/** + * Normalized params for {@link ApplyAllowlistUpdates}: every allowlist entry checksummed and + * duplicate-free, so {@link buildUnsigned} and the encoder never re-derive them. + */ +type ParsedApplyAllowlistUpdatesParams = { + poolAddress: string + removes: string[] + adds: string[] + sender?: string +} + +/** Encodes `applyAllowListUpdates` calldata against the resolved pool {@link Interface}. */ +type Encoder = (iface: Interface, params: ParsedApplyAllowlistUpdatesParams) => UnsignedEVMTx + +/** + * `removes` FIRST, then `adds` — the ABI's own parameter order. A swapped pair still encodes and + * still type-checks (both are `address[]`), and would silently allowlist the addresses meant to be + * revoked, so the byte-parity test is what pins this down. + */ +const encodeApplyAllowlistUpdates: Encoder = (iface, { poolAddress, removes, adds }) => + callTx(poolAddress, iface.encodeFunctionData('applyAllowListUpdates', [removes, adds])) + +/** + * Validates every entry of one array and returns it checksummed, rejecting duplicates. Compared + * on checksummed form, so the same address in two different casings still counts as a duplicate. + * + * The zero address is rejected outright: the pool skips it in `adds` (`if (toAdd == address(0)) + * continue`) and can never hold it, so it is a silent no-op in either array. + * @throws {@link CCTParamsInvalidError} if an entry is not a valid address or is the zero address + * (reported as `param[i]`), or the array holds duplicates + */ +function normalizeAddresses(operation: string, param: string, addresses: string[]): string[] { + const normalized = addresses.map((address, i) => { + validateNonZeroAddress(operation, `${param}[${i}]`, address) + return getAddress(address) + }) + if (new Set(normalized).size !== normalized.length) + throw new CCTParamsInvalidError(operation, param, 'must not contain duplicate addresses') + return normalized +} + +/** + * Applies allowlist removals and additions to an EVM token pool in one `applyAllowListUpdates` + * call (v1.5.0–v1.6.1; unsupported on v2.0.0, which has no allowlist). + */ +export class ApplyAllowlistUpdates extends EVMOperation< + ApplyAllowlistUpdatesParams, + ParsedApplyAllowlistUpdatesParams +> { + readonly name = 'applyAllowlistUpdates' + + /** + * One entry at V1_5_0 covers v1.5.0/v1.5.1/v1.6.1, whose signature is identical, and the + * explicit `null` at V2_0_0 stops the floor-match walk: the function was removed from the + * contract there, so there is nothing to inherit. See {@link resolveEncoder}. + */ + private readonly encoders: Partial> = { + [TokenPoolVersion.V1_5_0]: encodeApplyAllowlistUpdates, + [TokenPoolVersion.V2_0_0]: null, + } + + /** + * Validates the pool address and every allowlist entry before any RPC, *keeping* what each + * check produced (checksummed, duplicate-free arrays) so {@link buildUnsigned} and the encoder + * never re-derive it. + * + * Three judgement calls, all rejections: + * - **both arrays empty** — rejected: such a call encodes and mines while changing nothing, so + * it can only be a caller bug; mirrors `lockbox/operations/authorize-callers.ts`. + * - **duplicates within an array** — rejected, mirroring the Solana `configureAllowlist` / + * `removeFromAllowlist` ops. The EVM pool treats its allowlist as a set, so a duplicate is a + * silent no-op on-chain; catching it locally keeps the two families' contracts identical. + * - **an address in BOTH `adds` and `removes`** — rejected: removes apply first, so the address + * would end up *allowlisted*, and no caller can reasonably have meant both. + * + * - **the zero address in either array** — rejected: the pool `continue`s past it in `adds` and + * so can never hold it, making it a silent no-op on either side. + * + * Comparisons are on checksummed form, so the same address in two different casings still + * counts as a duplicate / an overlap. The remaining no-ops — removing an address that is not + * allowlisted, adding one that already is — need the pool's current allowlist and are caught in + * {@link buildUnsigned}. + * @throws {@link CCTParamsInvalidError} if `poolAddress` is invalid, either array is not an + * array or is sparse, both are empty, an entry is not a valid address or is the zero address + * (reported as `adds[i]` / `removes[i]`), an array holds duplicates, or an address appears in + * both arrays + */ + protected override parse(params: ApplyAllowlistUpdatesParams): ParsedApplyAllowlistUpdatesParams { + validateNonZeroAddress(this.name, 'poolAddress', params.poolAddress) + validateArray(this.name, 'removes', params.removes) + validateArray(this.name, 'adds', params.adds) + if (params.removes.length + params.adds.length === 0) { + throw new CCTParamsInvalidError( + this.name, + 'adds', + 'at least one address must be added or removed', + ) + } + + const removes = normalizeAddresses(this.name, 'removes', params.removes) + const adds = normalizeAddresses(this.name, 'adds', params.adds) + const removed = new Set(removes) + const overlap = adds.find((address) => removed.has(address)) + if (overlap !== undefined) { + throw new CCTParamsInvalidError( + this.name, + 'adds', + `${overlap} is also in removes; removes are applied first on-chain, so it would end up allowlisted — list it in one array only`, + ) + } + return { poolAddress: params.poolAddress, removes, adds, sender: params.sender } + } + + /** + * Resolves the pool's type + version, floor-matches the encoder (rejecting v2.0.0, which has no + * allowlist), confirms `sender` owns the pool when it is known, then pre-flights the update + * against the pool's current allowlist so nothing that would revert or mine as a no-op is ever + * built. + * + * Three state preconditions, all read in one round-trip by {@link readTokenPoolAllowlist}: + * - **the allowlist must be enabled** — `applyAllowListUpdates` opens with + * `if (!i_allowlistEnabled) revert AllowListNotEnabled()`. The flag is `immutable`, set to + * `allowlist.length > 0` in the constructor, so a pool deployed without one can never gain + * it: this is a permanent property of the pool, not a transient state. + * - **every `removes` entry must currently be allowlisted** — `EnumerableSet.remove` returns + * false for an absent address and the pool ignores it, so the tx mines having changed + * nothing. Mirrors `remove-remote-pool.ts`. + * - **no `adds` entry may already be allowlisted** — the symmetric case: `EnumerableSet.add` + * returns false and the entry is silently skipped. + * + * @remarks Encoder resolution runs *before* the owner read on purpose: an unsupported version + * should surface as {@link CCTOperationUnsupportedError} rather than burning an RPC on a pool + * this op can never target. The owner check is skipped entirely when `sender` is omitted — + * there is nothing to compare against, and `generateUnsignedApplyAllowlistUpdates` is expected + * to be usable before the eventual signer is known. {@link execute} always supplies one. The + * allowlist pre-flight, by contrast, does not depend on the signer and always runs. + * @throws {@link CCTOperationUnsupportedError} if the pool is v2.0.0 + * @throws {@link CCTContractTypeInvalidError} if the address is not a supported pool type + * @throws {@link CCTContractVersionUnsupportedError} if the pool reports an unknown version + * @throws {@link CCTParamsInvalidError} if `sender` is given and is not the pool owner, if the + * pool has no allowlist enabled, if a `removes` entry is not currently allowlisted, or if an + * `adds` entry already is + */ + protected async buildUnsigned( + chain: EVMChain, + params: ParsedApplyAllowlistUpdatesParams, + ): Promise { + const { type, version } = await resolveTokenPool(chain, params.poolAddress) + // resolved before any further RPC, so an unsupported version fails on one call + const encode = resolveEncoder(this.encoders, version, this.name) + // owner-gated on-chain; surface it as a param error here instead of an on-chain revert + if (params.sender !== undefined) + await assertPoolOwner(this.name, chain, params.poolAddress, params.sender) + + const { enabled, entries } = await readTokenPoolAllowlist(chain, params.poolAddress) + if (!enabled) + throw new CCTParamsInvalidError( + this.name, + 'poolAddress', + 'pool was deployed without an allowlist and can never have one (`allowlistEnabled` is immutable and false); applyAllowListUpdates reverts AllowListNotEnabled', + ) + + const allowlisted = new Set(entries) + const absent = params.removes.find((address) => !allowlisted.has(address)) + if (absent !== undefined) + throw new CCTParamsInvalidError( + this.name, + 'removes', + `${absent} is not allowlisted (allowlisted: ${entries.join(', ') || 'none'}); the pool would ignore it and the tx would change nothing`, + ) + const present = params.adds.find((address) => allowlisted.has(address)) + if (present !== undefined) + throw new CCTParamsInvalidError( + this.name, + 'adds', + `${present} is already allowlisted; the pool would ignore it and the tx would change nothing`, + ) + + chain.logger.debug( + `${this.name}: pool = ${params.poolAddress}, allowlisted = ${entries.length}, removes = ${params.removes.length}, adds = ${params.adds.length}`, + ) + return encode(getTokenPoolInterface(type, version), params) + } + + /** + * Signs and submits as the pool owner, defaulting `sender` to the signing wallet — the only + * address that can satisfy {@link buildUnsigned}'s owner check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} 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, + * if the wallet is not the pool owner, or if any other param is invalid + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} From 54a7ebe0a5a1e2a87b0c59d3f2491332d65ea2b6 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:08:34 +0100 Subject: [PATCH 79/87] chore(cct-sdk): Accept 1.5 proxy token pools (#397) * feat(cct-sdk): Add get token pool remotes evm query * feat(cct-sdk): Add generic EVM param validators * feat(cct-sdk): Read siloed lock/release pool state * feat(cct-sdk): Add pool owner pre-flight and encoder removal ceilings * refactor(cct-sdk): Hoist validate/parse lifecycle to the shared Operation base * feat(cct-sdk): Add apply chain updates evm op * fix(cct-sdk): Reject a declared version that is not the pool's calldata shape * feat(cct-sdk): Add EVM remote pool ops * Address PR comments * feat(cct-sdk): Add set chain rate limiter configs op * feat(cct-sdk): Add set rate limit admin and set dynamic config ops * feat(cct-sdk): Add apply allowlist updates evm op * chore(cct-sdk): Accept 1.5 proxy token pools * Fix lint * Fix lint * Fix lint * Fix lint * Fix lint * Address Mervin comments --- ccip-sdk/src/cct/evm/index.ts | 28 ++++++------- .../src/cct/evm/token-pool/contracts.test.ts | 40 +++++++++++++++++++ ccip-sdk/src/cct/evm/token-pool/contracts.ts | 13 ++++-- .../operations/get-token-pool-state.test.ts | 10 ++--- .../set-chain-rate-limiter-configs.ts | 13 +++--- ccip-sdk/src/cct/evm/validate.ts | 30 -------------- 6 files changed, 75 insertions(+), 59 deletions(-) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 786b85cc5..78905e5e1 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -598,14 +598,11 @@ export class EVMTokenManager extends TokenManager { /** * Replaces a v2.0.0 pool's dynamic config, signing + submitting with `opts.wallet`. `sender` * defaults to the wallet's address and must equal it — the wallet must be the pool owner. - * @remarks Replaces the config wholesale, so **all three params are required** — this op - * deliberately does *not* read `getDynamicConfig()` to fill in what the caller omitted, so an - * omitted field is reset rather than left alone. Read the current triple with - * {@link getTokenPoolState} and pass back the fields you are not changing, so what is submitted - * is exactly what was reviewed. - * - * Zero `rateLimitAdmin` / `feeAdmin` clear those delegations; `router` must be non-zero, since - * a zero router detaches the pool from CCIP rather than clearing a privilege. + * @remarks Writes all three fields in one call, so **all three params are required**: read the + * current triple with {@link getTokenPoolState} and pass back whatever you are not changing, as + * below. A missing field is a validation error, never "leave that one alone" — nothing is + * backfilled from `getDynamicConfig()`; see {@link generateUnsignedSetDynamicConfig} for why. + * On a 2.0.0 pool this replaces {@link setRateLimitAdmin}. * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer * @throws {@link CCTOperationUnsupportedError} on a pre-v2.0.0 pool — use {@link setRateLimitAdmin} * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not @@ -615,11 +612,14 @@ export class EVMTokenManager extends TokenManager { * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time * @example * ```typescript + * // change only rateLimitAdmin: read the current config and pass the rest back unchanged + * const state = await cct.getTokenPoolState({ poolAddress: '0xPool...' }) + * if (state.version !== '2.0.0') throw new Error('pre-2.0.0 pool: use setRateLimitAdmin') * const { hash } = await cct.setDynamicConfig({ * poolAddress: '0xPool...', - * router: '0xRouter...', + * router: state.router, * rateLimitAdmin: '0xOpsMultisig...', - * feeAdmin: '0xFeeMultisig...', + * feeAdmin: state.feeAdmin, * wallet, * }) * ``` @@ -830,10 +830,10 @@ export class EVMTokenManager extends TokenManager { * @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 `SiloedLockReleaseTokenPool` reports no `lockBox`, since it escrows per - * remote chain. 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. + * remote chain. For a legacy pool's `allowList` / `rebalancer`, proxy/USDC pools, or a v1.5.0 + * `*AndProxy` pool's `previousPool` (it reads here as its base `type`), 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 diff --git a/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts b/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts index 06c0e0e3c..a0f20f227 100644 --- a/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.test.ts @@ -146,6 +146,46 @@ describe('parseTokenPoolVersion', () => { ) }) + it('normalizes the v1.5.0 *AndProxy shims to their base type', () => { + for (const [contractType, type] of [ + ['BurnMintTokenPoolAndProxy', 'BurnMintTokenPool'], + ['BurnFromMintTokenPoolAndProxy', 'BurnFromMintTokenPool'], + ['BurnWithFromMintTokenPoolAndProxy', 'BurnWithFromMintTokenPool'], + ['LockReleaseTokenPoolAndProxy', 'LockReleaseTokenPool'], + ] as const) { + assert.deepEqual(parseTokenPoolVersion({ address: ADDR, contractType, version: '1.5.0' }), { + type, + version: TokenPoolVersion.V1_5_0, + }) + } + }) + + it('only strips AndProxy at v1.5.0 — the shim exists at no other version', () => { + for (const version of ['1.5.1', '1.6.1', '2.0.0']) { + assert.throws( + () => + parseTokenPoolVersion({ + address: ADDR, + contractType: 'BurnMintTokenPoolAndProxy', + version, + }), + CCTContractTypeInvalidError, + ) + } + }) + + it('gates the stripped base type, so an unsupported AndProxy name is still rejected', () => { + assert.throws( + () => + parseTokenPoolVersion({ + address: ADDR, + contractType: 'UpgradeableLockReleaseTokenPoolAndProxy', + version: '1.5.0', + }), + CCTContractTypeInvalidError, + ) + }) + it('throws CCTContractVersionUnsupportedError for an unknown version', () => { assert.throws( () => diff --git a/ccip-sdk/src/cct/evm/token-pool/contracts.ts b/ccip-sdk/src/cct/evm/token-pool/contracts.ts index be14ee687..2f8ba21f4 100644 --- a/ccip-sdk/src/cct/evm/token-pool/contracts.ts +++ b/ccip-sdk/src/cct/evm/token-pool/contracts.ts @@ -50,7 +50,8 @@ 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}. + * is distinct. Unsupported values fail in {@link parseTokenPoolVersion}, which also normalizes + * v1.5.0's `*AndProxy` shims onto these base names. */ export const TOKEN_POOL_TYPES = [ 'BurnMintTokenPool', @@ -109,7 +110,7 @@ export function isTokenPoolVersion(v: string): v is TokenPoolVersion { /** * Narrows raw `typeAndVersion` strings to a known {@link TokenPoolType} and - * {@link TokenPoolVersion}. + * {@link TokenPoolVersion}. A v1.5.0 `*AndProxy` type normalizes to its base pool type. * @throws {@link CCTContractTypeInvalidError} if `contractType` is not a supported pool type * @throws {@link CCTContractVersionUnsupportedError} if `version` is not a known pool version */ @@ -122,14 +123,18 @@ export function parseTokenPoolVersion({ contractType: string version: string }): { type: TokenPoolType; version: TokenPoolVersion } { - if (!isTokenPoolType(contractType)) + // v1.5.0's `*AndProxy` shims override only lockOrBurn/releaseOrMint, so every function a CCT op + // encodes is the base pool's — and the vendored v1.5.0 ABIs are the `*_and_proxy` ones already. + const type = + version === TokenPoolVersion.V1_5_0 ? contractType.replace(/AndProxy$/, '') : contractType + if (!isTokenPoolType(type)) throw new CCTContractTypeInvalidError(address, TOKEN_POOL_TYPES.join(', '), contractType) if (!isTokenPoolVersion(version)) throw new CCTContractVersionUnsupportedError(contractType, version, { context: { address }, }) - return { type: contractType, version } + return { type, version } } /** 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 index a71da720f..2eee5c089 100644 --- 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 @@ -293,17 +293,17 @@ describe('GetTokenPoolState (cct/evm token-pool query)', () => { assert.equal(state.tokenDecimals, 6) }) - it('rejects a v1.5.0 AndProxy pool, whose type name is not in the supported set', async () => { + it('reads a v1.5.0 AndProxy pool, reporting the normalized base type', 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, - ) + const state = await new GetTokenPoolState().query(chain, { poolAddress: POOL }) + + assert.equal(state.type, 'BurnMintTokenPool') + assert.equal(state.version, '1.5.0') }) }) diff --git a/ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.ts b/ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.ts index 098b7ccf4..e6581139b 100644 --- a/ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.ts +++ b/ccip-sdk/src/cct/evm/token-pool/operations/set-chain-rate-limiter-configs.ts @@ -20,7 +20,7 @@ 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 { assertDenseArray, validateNonZeroAddress, validateUint64 } from '../../validate.ts' +import { validateArray, validateNonZeroAddress, validateUint64 } from '../../validate.ts' import { TokenPoolVersion, getTokenPoolInterface, @@ -103,10 +103,7 @@ function parseUpdates( allowFastFinality: boolean, version: TokenPoolVersion | null, ): ParsedChainRateLimitUpdate[] { - if (!Array.isArray(updates) || updates.length === 0) - throw new CCTParamsInvalidError(operation, 'updates', 'must be a non-empty array') - // `.map` below skips holes, so reject a sparse array before it can smuggle one past validation - assertDenseArray(operation, 'updates', updates) + validateArray(operation, 'updates', updates, 1) const seen = new Set() return updates.map((update, i) => { @@ -328,7 +325,11 @@ export class SetChainRateLimiterConfigs extends EVMOperation Date: Sat, 5 Sep 2026 00:50:27 +0800 Subject: [PATCH 80/87] feat(cct-sdk): Add update metadata authority op solana (#387) * feat: add transfer pool ownership op solana * feat: add accept pool ownership op solana * feat: add transfer authority op solana * fix: update export barrel * feat: add mint tokens op solana * fix: address comments * fix: address comments * feat: add set can accept liquidity op solana * fix: add lock release token pool idl * feat: add set rebalancer op solana * fix: update tsdoc * feat: add approve token op solana * feat: add provider liquidity op solana * fix: address comments * fix: revert unrelated changes * fix: add preflight checks * fix: update tsdoc * fix: extract validate pool liquidity config * feat: add withdraw liquidity op solana * fix: add preflight checks * feat: add update metadata authority op solana * fix: lint errors * fix: lint errors * fix: address comments * fix(cct-sdk): Refactor and expand Solana CCT unit tests (#395) * fix: refactor unit tests * fix: refactor export --- ccip-sdk/src/cct/solana/index.test.ts | 672 ++++++++++++++++-- ccip-sdk/src/cct/solana/index.ts | 77 +- ccip-sdk/src/cct/solana/programs/token.ts | 11 + .../operations/accept-admin.test.ts | 66 +- .../operations/append-to-lookup-table.test.ts | 104 ++- .../operations/create-lookup-table.test.ts | 56 +- .../get-token-admin-registry.test.ts | 27 +- .../token-admin-registry/operations/index.ts | 9 +- .../operations/register-admin.test.ts | 49 +- .../operations/set-pool.test.ts | 39 +- .../operations/transfer-admin.test.ts | 72 +- .../operations/accept-ownership.test.ts | 16 +- .../append-remote-pool-addresses.test.ts | 8 +- .../operations/apply-chain-updates.test.ts | 61 +- .../operations/configure-allowlist.test.ts | 24 +- .../operations/create-token-multisig.test.ts | 77 +- .../delete-chain-remote-config.test.ts | 8 +- .../operations/deploy-token-pool.test.ts | 44 +- .../edit-chain-remote-config.test.ts | 8 +- .../operations/get-token-pool-remotes.test.ts | 8 +- .../init-chain-remote-config.test.ts | 8 +- .../operations/provide-liquidity.test.ts | 45 +- .../operations/provide-liquidity.ts | 16 +- .../operations/remove-from-allowlist.test.ts | 12 +- .../set-can-accept-liquidity.test.ts | 8 +- .../operations/set-chain-rate-limit.test.ts | 8 +- .../operations/set-rate-limit-admin.test.ts | 8 +- .../operations/set-rebalancer.test.ts | 8 +- .../operations/transfer-ownership.test.ts | 8 +- .../operations/withdraw-liquidity.test.ts | 42 +- ccip-sdk/src/cct/solana/token/constants.ts | 5 + .../token/operations/approve-token.test.ts | 22 +- .../operations/create-token-account.test.ts | 46 +- .../token/operations/deploy-token.test.ts | 126 +++- .../solana/token/operations/deploy-token.ts | 12 +- .../src/cct/solana/token/operations/index.ts | 1 + .../token/operations/mint-tokens.test.ts | 12 +- .../operations/set-token-authority.test.ts | 10 +- .../update-metadata-authority.test.ts | 174 +++++ .../operations/update-metadata-authority.ts | 190 +++++ ccip-sdk/src/cct/solana/validate.test.ts | 102 ++- 41 files changed, 1892 insertions(+), 407 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/programs/token.ts create mode 100644 ccip-sdk/src/cct/solana/token/operations/update-metadata-authority.test.ts create mode 100644 ccip-sdk/src/cct/solana/token/operations/update-metadata-authority.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 7c2195428..18d96c10c 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -1,13 +1,22 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' -import { Connection } from '@solana/web3.js' +import { BorshAccountsCoder } from '@coral-xyz/anchor' +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { Connection, Keypair, PublicKey } from '@solana/web3.js' import { SolanaChain } from '../../solana/index.ts' +import { deriveTokenAdminRegistryPda } from './programs/router.ts' +import { + TOKEN_POOL_PROGRAMS, + deriveTokenPoolConfigPda, + deriveTokenPoolSignerPda, +} from './programs/token-pool.ts' import type { GetTokenPoolStateParams, GetTokenPoolStateResult, } from './token-pool/operations/index.ts' +import { METADATA_PROGRAM_ID } from './token/constants.ts' import { type RegisterAdminMethod, type TokenAuthorityType, @@ -25,76 +34,6 @@ function stubChain(): 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.generateUnsignedApproveToken, 'function') - assert.equal(typeof cct.approveToken, 'function') - assert.equal(typeof cct.generateUnsignedCreateTokenAccount, 'function') - assert.equal(typeof cct.createTokenAccount, 'function') - assert.equal(typeof cct.generateUnsignedMintTokens, 'function') - assert.equal(typeof cct.mintTokens, 'function') - assert.equal(typeof cct.generateUnsignedSetTokenAuthority, 'function') - assert.equal(typeof cct.setTokenAuthority, '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.generateUnsignedSetCanAcceptLiquidity, 'function') - assert.equal(typeof cct.setCanAcceptLiquidity, '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.generateUnsignedProvideLiquidity, 'function') - assert.equal(typeof cct.provideLiquidity, 'function') - assert.equal(typeof cct.generateUnsignedWithdrawLiquidity, 'function') - assert.equal(typeof cct.withdrawLiquidity, 'function') - assert.equal(typeof cct.generateUnsignedSetRebalancer, 'function') - assert.equal(typeof cct.setRebalancer, '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('exports public CCT constants', () => { const authorityType: TokenAuthorityType = TOKEN_AUTHORITY_TYPES.MINT const method: RegisterAdminMethod = REGISTRATION_METHODS.OWNER @@ -141,4 +80,595 @@ describe('SolanaTokenManager (cct/solana)', () => { assert.equal(typeof read, 'function') }) + + describe('facade operations', () => { + const payer = Keypair.generate().publicKey.toBase58() + const mint = Keypair.generate().publicKey.toBase58() + const pool = Keypair.generate().publicKey.toBase58() + const account = Keypair.generate().publicKey.toBase58() + const reader = Keypair.generate().publicKey.toBase58() + const remoteChainSelector = 5009297550715157269n + + function chain(): SolanaChain { + const mintAccount = Buffer.alloc(82) + mintAccount.writeUInt32LE(1, 0) + new PublicKey(payer).toBuffer().copy(mintAccount, 4) + mintAccount[44] = 6 + mintAccount[45] = 1 + const tokenAccount = Buffer.alloc(165) + new PublicKey(mint).toBuffer().copy(tokenAccount, 0) + new PublicKey(payer).toBuffer().copy(tokenAccount, 32) + tokenAccount.writeBigUInt64LE(1n, 64) + tokenAccount.writeUInt32LE(1, 72) + deriveTokenPoolSignerPda( + new PublicKey(TOKEN_POOL_PROGRAMS['lock-release']), + new PublicKey(mint), + ) + .toBuffer() + .copy(tokenAccount, 76) + tokenAccount[108] = 1 + tokenAccount.writeBigUInt64LE(1n, 121) + const poolProgram = new PublicKey(TOKEN_POOL_PROGRAMS['lock-release']) + const poolStateAddress = deriveTokenPoolConfigPda(poolProgram, new PublicKey(mint)) + const registryAddress = deriveTokenAdminRegistryPda( + new PublicKey(account), + new PublicKey(mint), + ) + const readerRegistryAddress = deriveTokenAdminRegistryPda( + new PublicKey(pool), + new PublicKey(mint), + ) + const registry = Buffer.alloc(170) + BorshAccountsCoder.accountDiscriminator('TokenAdminRegistry').copy(registry) + registry[8] = 2 + new PublicKey(payer).toBuffer().copy(registry, 9) + new PublicKey(payer).toBuffer().copy(registry, 41) + new PublicKey(account).toBuffer().copy(registry, 73) + registry[120] = 0x19 + new PublicKey(mint).toBuffer().copy(registry, 137) + registry[169] = 1 + const [metadataAddress] = PublicKey.findProgramAddressSync( + [Buffer.from('metadata'), METADATA_PROGRAM_ID.toBuffer(), new PublicKey(mint).toBuffer()], + METADATA_PROGRAM_ID, + ) + const metadata = Buffer.concat([ + Buffer.from([4]), + new PublicKey(payer).toBuffer(), + new PublicKey(mint).toBuffer(), + Buffer.alloc(14), + Buffer.from([0, 0, 1, 0, 0, 0, 0, 0]), + ]) + const poolState = Buffer.concat([ + BorshAccountsCoder.accountDiscriminator('State'), + Buffer.from([1]), + poolProgram.toBuffer(), + new PublicKey(mint).toBuffer(), + Buffer.from([6]), + ...Array.from({ length: 8 }, () => new PublicKey(payer).toBuffer()), + Buffer.from([1, 1, 0, 0, 0, 0]), + new PublicKey(payer).toBuffer(), + new PublicKey(payer).toBuffer(), + new PublicKey(payer).toBuffer(), + ]) + const accounts = new Map([ + [new PublicKey(mint).toBase58(), { owner: TOKEN_PROGRAM_ID, data: mintAccount }], + [metadataAddress.toBase58(), { owner: METADATA_PROGRAM_ID, data: metadata }], + [poolStateAddress.toBase58(), { owner: poolProgram, data: poolState }], + [registryAddress.toBase58(), null], + [readerRegistryAddress.toBase58(), { data: registry }], + ]) + const defaultAccount = { owner: TOKEN_PROGRAM_ID, data: tokenAccount } + + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + rpcEndpoint: 'http://localhost:8899', + getAccountInfo: async (address: PublicKey) => { + const key = address.toBase58() + return accounts.has(key) ? accounts.get(key) : defaultAccount + }, + getMinimumBalanceForRentExemption: async () => 1, + getSlot: async () => 1, + getAddressLookupTable: async () => ({ + value: { + state: { + authority: new PublicKey(payer), + addresses: [ + PublicKey.default, + PublicKey.default, + PublicKey.default, + new PublicKey(pool), + ], + }, + }, + }), + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => PublicKey.default.toBase58(), + confirmTransaction: async () => ({ value: { err: null } }), + }, + getTokenAdminRegistryFor: async (address: string) => (address === reader ? pool : account), + getSupportedTokens: async () => [mint], + getTokenPoolRemotes: async () => ({}), + getRegistryTokenConfig: async () => ({ administrator: payer, pendingAdministrator: payer }), + } as unknown as SolanaChain + } + + const facadeChain = chain() + + it('runs every unsigned facade operation', async () => { + const cct = SolanaTokenManager.fromChain(facadeChain) + const common = { + payer, + tokenAddress: mint, + authority: payer, + poolType: 'lock-release' as const, + } + const cases: Array< + [string, () => Promise<{ instructions: unknown[] } | { instructions: unknown[] }[]>] + > = [ + [ + 'deployToken', + () => cct.generateUnsignedDeployToken({ payer, decimals: 6, withMetaplex: false }), + ], + [ + 'approveToken', + () => cct.generateUnsignedApproveToken({ ...common, delegate: account, amount: 1n }), + ], + [ + 'createTokenAccount', + () => + cct.generateUnsignedCreateTokenAccount({ + payer, + tokenAddress: mint, + ownerAddress: account, + }), + ], + [ + 'mintTokens', + () => cct.generateUnsignedMintTokens({ ...common, recipient: account, amount: 1n }), + ], + [ + 'setTokenAuthority', + () => + cct.generateUnsignedSetTokenAuthority({ + ...common, + newAuthority: account, + authorityTypes: ['mint'], + }), + ], + [ + 'updateMetadataAuthority', + () => cct.generateUnsignedUpdateMetadataAuthority({ ...common, newAuthority: account }), + ], + [ + 'createTokenMultisig', + () => + cct.generateUnsignedCreateTokenMultisig({ + payer, + tokenAddress: mint, + poolType: 'lock-release', + threshold: 1, + }), + ], + [ + 'createLookupTable', + () => + cct.generateUnsignedCreateLookupTable({ payer, authority: payer, mode: 'createEmpty' }), + ], + [ + 'configureAllowlist', + () => + cct.generateUnsignedConfigureAllowlist({ ...common, add: [account], enabled: true }), + ], + ['deployTokenPool', () => cct.generateUnsignedDeployTokenPool(common)], + [ + 'applyChainUpdates', + () => + cct.generateUnsignedApplyChainUpdates({ + ...common, + remoteChainSelectorsToRemove: [], + chainsToAdd: [ + { + remoteChainSelector, + remoteTokenAddress: '0x01', + remotePoolAddresses: ['0x02'], + remoteTokenDecimals: 6, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: false }, + }, + ], + }), + ], + [ + 'appendRemotePoolAddresses', + () => + cct.generateUnsignedAppendRemotePoolAddresses({ + ...common, + remoteChainSelector, + remotePoolAddresses: ['0x01'], + }), + ], + [ + 'initChainRemoteConfig', + () => + cct.generateUnsignedInitChainRemoteConfig({ + ...common, + remoteChainSelector, + remoteTokenAddress: '0x01', + remoteTokenDecimals: 6, + }), + ], + [ + 'deleteChainRemoteConfig', + () => cct.generateUnsignedDeleteChainRemoteConfig({ ...common, remoteChainSelector }), + ], + [ + 'setRateLimitAdmin', + () => cct.generateUnsignedSetRateLimitAdmin({ ...common, newRateLimitAdmin: account }), + ], + ['provideLiquidity', () => cct.generateUnsignedProvideLiquidity({ ...common, amount: 1n })], + [ + 'withdrawLiquidity', + () => cct.generateUnsignedWithdrawLiquidity({ ...common, amount: 1n }), + ], + [ + 'setCanAcceptLiquidity', + () => cct.generateUnsignedSetCanAcceptLiquidity({ ...common, allow: true }), + ], + [ + 'setRebalancer', + () => cct.generateUnsignedSetRebalancer({ ...common, rebalancer: account }), + ], + [ + 'transferOwnership', + () => cct.generateUnsignedTransferOwnership({ ...common, newOwner: account }), + ], + ['acceptOwnership', () => cct.generateUnsignedAcceptOwnership(common)], + [ + 'setChainRateLimit', + () => + cct.generateUnsignedSetChainRateLimit({ + ...common, + remoteChainSelector, + inbound: { enabled: false }, + outbound: { enabled: false }, + }), + ], + [ + 'editChainRemoteConfig', + () => + cct.generateUnsignedEditChainRemoteConfig({ + ...common, + remoteChainSelector, + remoteTokenAddress: '0x01', + remotePoolAddresses: ['0x02'], + remoteTokenDecimals: 6, + }), + ], + [ + 'appendToLookupTable', + () => + cct.generateUnsignedAppendToLookupTable({ + payer, + lookupTableAddress: account, + additionalAddresses: [mint], + }), + ], + ['acceptAdmin', () => cct.generateUnsignedAcceptAdmin({ ...common, address: account })], + ['registerAdmin', () => cct.generateUnsignedRegisterAdmin({ ...common, address: account })], + [ + 'removeFromAllowlist', + () => cct.generateUnsignedRemoveFromAllowlist({ ...common, remove: [account] }), + ], + [ + 'setPool', + () => + cct.generateUnsignedSetPool({ + ...common, + address: account, + poolLookupTableAddress: account, + }), + ], + [ + 'transferAdmin', + () => + cct.generateUnsignedTransferAdmin({ ...common, address: account, newAdmin: account }), + ], + ] + + for (const [name, operation] of cases) { + const result = await operation() + assert.ok( + (Array.isArray(result) ? result[0] : result)?.instructions.length, + `${name} returns instructions`, + ) + } + }) + + it('runs every signed facade operation', async () => { + const cct = SolanaTokenManager.fromChain(facadeChain) + const wallet = { publicKey: new PublicKey(payer), signTransaction: async (tx: T) => tx } + const signed: Array< + [string, () => Promise<{ hash: string } | { hash: string }[] | { hashes: string[] }>] + > = [ + ['deployToken', () => cct.deployToken({ wallet, decimals: 6, withMetaplex: false })], + [ + 'approveToken', + () => cct.approveToken({ wallet, tokenAddress: mint, delegate: account, amount: 1n }), + ], + [ + 'createTokenAccount', + () => cct.createTokenAccount({ wallet, tokenAddress: mint, ownerAddress: account }), + ], + [ + 'mintTokens', + () => cct.mintTokens({ wallet, tokenAddress: mint, recipient: account, amount: 1n }), + ], + [ + 'setTokenAuthority', + () => + cct.setTokenAuthority({ + wallet, + tokenAddress: mint, + newAuthority: account, + authorityTypes: ['mint'], + }), + ], + [ + 'updateMetadataAuthority', + () => cct.updateMetadataAuthority({ wallet, tokenAddress: mint, newAuthority: account }), + ], + [ + 'createTokenMultisig', + () => + cct.createTokenMultisig({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + threshold: 1, + }), + ], + ['createLookupTable', () => cct.createLookupTable({ wallet, mode: 'createEmpty' })], + [ + 'configureAllowlist', + () => + cct.configureAllowlist({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + add: [account], + enabled: true, + }), + ], + [ + 'deployTokenPool', + () => cct.deployTokenPool({ wallet, tokenAddress: mint, poolType: 'lock-release' }), + ], + [ + 'applyChainUpdates', + () => + cct.applyChainUpdates({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + remoteChainSelectorsToRemove: [], + chainsToAdd: [ + { + remoteChainSelector, + remoteTokenAddress: '0x01', + remotePoolAddresses: ['0x02'], + remoteTokenDecimals: 6, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: false }, + }, + ], + }), + ], + [ + 'appendRemotePoolAddresses', + () => + cct.appendRemotePoolAddresses({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + remoteChainSelector, + remotePoolAddresses: ['0x01'], + }), + ], + [ + 'initChainRemoteConfig', + () => + cct.initChainRemoteConfig({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + remoteChainSelector, + remoteTokenAddress: '0x01', + remoteTokenDecimals: 6, + }), + ], + [ + 'deleteChainRemoteConfig', + () => + cct.deleteChainRemoteConfig({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + remoteChainSelector, + }), + ], + [ + 'setRateLimitAdmin', + () => + cct.setRateLimitAdmin({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + newRateLimitAdmin: account, + }), + ], + [ + 'provideLiquidity', + () => + cct.provideLiquidity({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + amount: 1n, + }), + ], + [ + 'withdrawLiquidity', + () => + cct.withdrawLiquidity({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + amount: 1n, + }), + ], + [ + 'setCanAcceptLiquidity', + () => + cct.setCanAcceptLiquidity({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + allow: true, + }), + ], + [ + 'setRebalancer', + () => + cct.setRebalancer({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + rebalancer: account, + }), + ], + [ + 'transferOwnership', + () => + cct.transferOwnership({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + newOwner: account, + }), + ], + [ + 'acceptOwnership', + () => cct.acceptOwnership({ wallet, tokenAddress: mint, poolType: 'lock-release' }), + ], + [ + 'setChainRateLimit', + () => + cct.setChainRateLimit({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + remoteChainSelector, + inbound: { enabled: false }, + outbound: { enabled: false }, + }), + ], + [ + 'editChainRemoteConfig', + () => + cct.editChainRemoteConfig({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + remoteChainSelector, + remoteTokenAddress: '0x01', + remotePoolAddresses: ['0x02'], + remoteTokenDecimals: 6, + }), + ], + [ + 'appendToLookupTable', + () => + cct.appendToLookupTable({ + wallet, + lookupTableAddress: account, + additionalAddresses: [mint], + }), + ], + ['acceptAdmin', () => cct.acceptAdmin({ wallet, tokenAddress: mint, address: account })], + [ + 'registerAdmin', + () => cct.registerAdmin({ wallet, tokenAddress: mint, address: account }), + ], + [ + 'removeFromAllowlist', + () => + cct.removeFromAllowlist({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + remove: [account], + }), + ], + [ + 'setPool', + () => + cct.setPool({ + wallet, + tokenAddress: mint, + address: account, + poolLookupTableAddress: account, + }), + ], + [ + 'transferAdmin', + () => + cct.transferAdmin({ wallet, tokenAddress: mint, address: account, newAdmin: account }), + ], + ] + + for (const [name, operation] of signed) { + const result = await operation() + const hashes = Array.isArray(result) + ? result.map(({ hash }) => hash) + : 'hashes' in result + ? result.hashes + : [result.hash] + assert.ok(hashes.length && hashes.every(Boolean), `${name} returns transaction hashes`) + } + }) + + it('runs every read facade operation', async () => { + const cct = SolanaTokenManager.fromChain(facadeChain) + const reads: Array<[string, () => Promise]> = [ + [ + 'getTokenPoolRemotes', + () => + cct.getTokenPoolRemotes({ + tokenAddress: mint, + poolType: 'lock-release', + remoteChainSelector, + }), + ], + [ + 'getTokenPoolState', + () => cct.getTokenPoolState({ tokenAddress: mint, poolType: 'lock-release' }), + ], + [ + 'getTokenAdminRegistry', + () => cct.getTokenAdminRegistry({ tokenAddress: mint, address: reader }), + ], + ['getSupportedTokens', () => cct.getSupportedTokens({ address: reader })], + ] + + for (const [name, read] of reads) { + const result = await read() + assert.ok(typeof result === 'object', `${name} returns a result`) + } + }) + }) }) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 0f093a16d..c9dc5a4a0 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -158,6 +158,8 @@ import { type ExecuteMintTokensResult, type ExecuteSetTokenAuthorityParams, type ExecuteSetTokenAuthorityResult, + type ExecuteUpdateMetadataAuthorityParams, + type ExecuteUpdateMetadataAuthorityResult, type GenerateApproveTokenParams, type GenerateApproveTokenResult, type GenerateCreateTokenAccountParams, @@ -168,10 +170,13 @@ import { type GenerateMintTokensResult, type GenerateSetTokenAuthorityParams, type GenerateSetTokenAuthorityResult, + type GenerateUpdateMetadataAuthorityParams, + type GenerateUpdateMetadataAuthorityResult, ApproveToken, CreateTokenAccount, MintTokens, SetTokenAuthority, + UpdateMetadataAuthority, } from './token/operations/index.ts' /** CCT admin facade for Solana. */ @@ -182,6 +187,7 @@ export class SolanaTokenManager extends TokenManager readonly #createTokenAccount = new CreateTokenAccount() readonly #mintTokens = new MintTokens() readonly #setTokenAuthority = new SetTokenAuthority() + readonly #updateMetadataAuthority = new UpdateMetadataAuthority() // Token admin registry operations readonly #acceptAdmin = new AcceptAdmin() @@ -246,6 +252,8 @@ export class SolanaTokenManager extends TokenManager * Builds unsigned Solana mint creation instructions, optionally with initial supply. * The `payer` defaults as mint, freeze, and metadata update authority. * + * @see {@link updateMetadataAuthority} To transfer the initial metadata update authority. + * * @throws {@link CCTParamsInvalidError} If token parameters are invalid. * * @example @@ -272,6 +280,8 @@ export class SolanaTokenManager extends TokenManager * Creates a Solana mint, optionally with initial supply. * The wallet public key defaults as mint, freeze, and metadata update authority. * + * @see {@link updateMetadataAuthority} To transfer the initial 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. @@ -559,6 +569,69 @@ export class SolanaTokenManager extends TokenManager return this.#setTokenAuthority.execute(this.chain, opts) } + /** + * Builds unsigned instructions to transfer a token's Metaplex metadata update authority. + * + * @see {@link updateMetadataAuthority} For wallet-based execution. + * @see {@link setTokenAuthority} For SPL mint and freeze authority changes. + * @see {@link deployToken} To set the initial metadata update authority. + * + * @remarks + * The mint must have mutable Metaplex Token Metadata and `authority` must match its current + * update authority. `authority` defaults to `payer`; both the payer and authority must sign if + * they differ. Use this to hand metadata control to a multisig or DAO after deployment. + * + * @throws {@link CCTParamsInvalidError} If an address is invalid, the mint has no Metaplex + * metadata, or `authority` is not its current metadata update authority. + * @throws {@link CCTTxFailedError} If the metadata is immutable. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const unsigned = await cct.generateUnsignedUpdateMetadataAuthority({ + * payer: currentAuthority, + * tokenAddress: mint, + * newAuthority, + * }) + * ``` + */ + generateUnsignedUpdateMetadataAuthority( + opts: GenerateUpdateMetadataAuthorityParams, + ): Promise { + return this.#updateMetadataAuthority.generate(this.chain, opts) + } + + /** + * Transfers a token's Metaplex metadata update authority using the executing wallet. + * + * @see {@link generateUnsignedUpdateMetadataAuthority} For externally signed transactions. + * @see {@link setTokenAuthority} For SPL mint and freeze authority changes. + * @see {@link deployToken} To set the initial metadata update authority. + * + * @remarks + * The mint must have mutable Metaplex Token Metadata and the executing wallet must be its + * current update authority. Use this to hand metadata control to a multisig or DAO after + * deployment. Use {@link generateUnsignedUpdateMetadataAuthority} when payer and authority + * differ or external signatures are required. + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address is invalid, the mint has no Metaplex + * metadata, or `authority` does not match the metadata or executing wallet. + * @throws {@link CCTTxFailedError} If the metadata is immutable, simulation fails, or the Metaplex + * program rejects the transaction. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.updateMetadataAuthority({ wallet, tokenAddress: mint, newAuthority }) + * ``` + */ + updateMetadataAuthority( + opts: ExecuteUpdateMetadataAuthorityParams, + ): Promise { + return this.#updateMetadataAuthority.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. @@ -2152,7 +2225,9 @@ export class SolanaTokenManager extends TokenManager * fields. Pass `poolProgramAddress` instead of `poolType` for a custom pool program. */ getTokenPoolState( - opts: (BurnMintPoolProgramRef | CustomPoolProgramRef) & { tokenAddress: string }, + opts: (BurnMintPoolProgramRef | CustomPoolProgramRef) & { + tokenAddress: string + }, ): Promise /** * Reads a pool state account whose program is not known statically; narrow the result on the diff --git a/ccip-sdk/src/cct/solana/programs/token.ts b/ccip-sdk/src/cct/solana/programs/token.ts new file mode 100644 index 000000000..0e7f93eb7 --- /dev/null +++ b/ccip-sdk/src/cct/solana/programs/token.ts @@ -0,0 +1,11 @@ +import { PublicKey } from '@solana/web3.js' + +import { METADATA_PROGRAM_ID } from '../token/constants.ts' + +/** Derives the Metaplex metadata PDA for a mint. */ +export function deriveMetadataAddress(mint: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('metadata'), METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()], + METADATA_PROGRAM_ID, + )[0] +} 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 index 0f1935dcc..cdb295878 100644 --- 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 @@ -3,20 +3,21 @@ 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' import { deriveRouterConfigPda, deriveTokenAdminRegistryPda } from '../../programs/router.ts' -import type { GenerateAcceptAdminParams } from './accept-admin.ts' +import { type GenerateAcceptAdminParams, AcceptAdmin } from './accept-admin.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 HASH = Keypair.generate().publicKey.toBase58() const WALLET = { - publicKey: Keypair.generate().publicKey, + publicKey: new PublicKey(PENDING_ADMIN), signTransaction: async (tx: T) => tx, } @@ -35,8 +36,22 @@ function stubChain( } as unknown as SolanaChain } +function submitChain(): SolanaChain { + return Object.assign(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 } }), + }, + }) +} + function generate(opts: Partial = {}) { - return SolanaTokenManager.fromChain(stubChain()).generateUnsignedAcceptAdmin({ + return new AcceptAdmin().generate(stubChain(), { tokenAddress: TOKEN, address: ADDRESS, payer: PAYER, @@ -84,17 +99,16 @@ describe('AcceptAdmin (cct/solana)', () => { it('resolves the router from address', async () => { let requestedAddress: string | undefined - const cct = SolanaTokenManager.fromChain( + await new AcceptAdmin().generate( stubChain(PENDING_ADMIN, (address) => (requestedAddress = address)), + { + tokenAddress: TOKEN, + address: ADDRESS, + payer: PAYER, + authority: PENDING_ADMIN, + }, ) - await cct.generateUnsignedAcceptAdmin({ - tokenAddress: TOKEN, - address: ADDRESS, - payer: PAYER, - authority: PENDING_ADMIN, - }) - assert.equal(requestedAddress, ADDRESS) }) }) @@ -117,7 +131,7 @@ describe('AcceptAdmin (cct/solana)', () => { await assert.rejects( () => - SolanaTokenManager.fromChain(noPendingChain).generateUnsignedAcceptAdmin({ + new AcceptAdmin().generate(noPendingChain, { tokenAddress: TOKEN, address: ADDRESS, payer: PAYER, @@ -132,13 +146,35 @@ describe('AcceptAdmin (cct/solana)', () => { }) describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + assert.deepEqual( + await new AcceptAdmin().execute(submitChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + wallet: WALLET, + }), + { hash: HASH }, + ) + }) + + it('rejects an invalid wallet before generating instructions', async () => { + await assert.rejects( + new AcceptAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + wallet: {}, + }), + CCIPWalletInvalidError, + ) + }) + it('requires the pending admin to be the executing wallet', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain()).acceptAdmin({ + new AcceptAdmin().execute(stubChain(), { tokenAddress: TOKEN, address: ADDRESS, - authority: PENDING_ADMIN, + authority: PAYER, wallet: WALLET, }), (err: unknown) => 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 index 39d83a7e7..7487e9fca 100644 --- 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 @@ -7,9 +7,9 @@ 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' +import { AppendToLookupTable } from './append-to-lookup-table.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const POOL_PROGRAM = Keypair.generate().publicKey.toBase58() @@ -19,21 +19,24 @@ 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 HASH = Keypair.generate().publicKey.toBase58() const WALLET = { - publicKey: Keypair.generate().publicKey, + publicKey: new PublicKey(AUTHORITY), signTransaction: async (tx: T) => tx, } type StubChainOptions = { addresses?: PublicKey[] - authority?: string + authority?: string | null onGetLookupTable?: () => void + missingLookupTable?: boolean } function stubChain({ addresses = [], authority = AUTHORITY, onGetLookupTable, + missingLookupTable = false, }: StubChainOptions = {}): SolanaChain { return { logger: { debug() {}, info() {}, warn() {}, error() {} }, @@ -42,14 +45,23 @@ function stubChain({ getAddressLookupTable: async () => { onGetLookupTable?.() return { - value: { - state: { - authority: new PublicKey(authority), - addresses, - }, - }, + value: missingLookupTable + ? null + : { + state: { + authority: authority ? new PublicKey(authority) : undefined, + addresses, + }, + }, } }, + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), }, getTokenPoolConfig: async () => ({ token: TOKEN, @@ -61,7 +73,7 @@ function stubChain({ } function generate(opts = {}, chain = stubChain()) { - return SolanaTokenManager.fromChain(chain).generateUnsignedAppendToLookupTable({ + return new AppendToLookupTable().generate(chain, { lookupTableAddress: LOOKUP_TABLE, payer: PAYER, authority: AUTHORITY, @@ -171,6 +183,16 @@ describe('AppendToLookupTable (cct/solana)', () => { ) }) + it('defaults omitted additional addresses to an empty list', async () => { + const unsigned = await generate({ + additionalAddresses: undefined, + tokenAddress: TOKEN, + poolProgramAddress: POOL_PROGRAM, + }) + + assert.equal(unsigned.instructions.length, 1) + }) + it('rejects authority mismatch', async () => { await assert.rejects( () => generate({}, stubChain({ authority: Keypair.generate().publicKey.toBase58() })), @@ -181,6 +203,13 @@ describe('AppendToLookupTable (cct/solana)', () => { ) }) + it('rejects an ALT with no authority', async () => { + await assert.rejects( + () => generate({}, stubChain({ authority: null })), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'authority', + ) + }) + it('rejects ALTs over 256 addresses', async () => { const currentAddresses = Array.from({ length: 256 }, () => Keypair.generate().publicKey) @@ -195,19 +224,28 @@ describe('AppendToLookupTable (cct/solana)', () => { }) describe('validation', () => { + it('rejects a missing lookup table', async () => { + await assert.rejects( + () => generate({}, stubChain({ missingLookupTable: true })), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'lookupTableAddress', + ) + }) + it('rejects an ambiguous pool reference before the ALT RPC', async () => { let getLookupTableCalls = 0 await assert.rejects( - SolanaTokenManager.fromChain( + new AppendToLookupTable().generate( stubChain({ onGetLookupTable: () => getLookupTableCalls++ }), - ).generateUnsignedAppendToLookupTable({ - lookupTableAddress: LOOKUP_TABLE, - payer: PAYER, - tokenAddress: TOKEN, - poolType: 'burn-mint', - poolProgramAddress: POOL_PROGRAM, - } as never), + { + lookupTableAddress: LOOKUP_TABLE, + payer: PAYER, + tokenAddress: TOKEN, + poolType: 'burn-mint', + poolProgramAddress: POOL_PROGRAM, + } as never, + ), CCTParamsInvalidError, ) @@ -218,14 +256,15 @@ describe('AppendToLookupTable (cct/solana)', () => { let getLookupTableCalls = 0 await assert.rejects( - SolanaTokenManager.fromChain( + new AppendToLookupTable().generate( stubChain({ onGetLookupTable: () => getLookupTableCalls++ }), - ).generateUnsignedAppendToLookupTable({ - lookupTableAddress: LOOKUP_TABLE, - payer: PAYER, - tokenAddress: TOKEN, - poolProgramAddress: 'invalid', - }), + { + lookupTableAddress: LOOKUP_TABLE, + payer: PAYER, + tokenAddress: TOKEN, + poolProgramAddress: 'invalid', + }, + ), CCTParamsInvalidError, ) @@ -254,13 +293,24 @@ describe('AppendToLookupTable (cct/solana)', () => { }) describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + assert.deepEqual( + await new AppendToLookupTable().execute(stubChain(), { + lookupTableAddress: LOOKUP_TABLE, + wallet: WALLET, + additionalAddresses: [Keypair.generate().publicKey.toBase58()], + }), + { hash: HASH }, + ) + }) + it('rejects signed append when authority is not the wallet', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain()).appendToLookupTable({ + new AppendToLookupTable().execute(stubChain(), { lookupTableAddress: LOOKUP_TABLE, wallet: WALLET, - authority: AUTHORITY, + authority: PAYER, additionalAddresses: [Keypair.generate().publicKey.toBase58()], }), (err: unknown) => 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 index e8231117c..f87446cdb 100644 --- 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 @@ -7,8 +7,8 @@ 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' +import { CreateLookupTable } from './create-lookup-table.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const POOL_PROGRAM = Keypair.generate().publicKey.toBase58() @@ -16,8 +16,9 @@ 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 HASH = Keypair.generate().publicKey.toBase58() const WALLET = { - publicKey: Keypair.generate().publicKey, + publicKey: new PublicKey(AUTHORITY), signTransaction: async (tx: T) => tx, } @@ -30,6 +31,13 @@ function stubChain(onGetSlot?: () => void): SolanaChain { return 123 }, getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }), + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), }, getTokenPoolConfig: async () => ({ token: TOKEN, @@ -41,7 +49,7 @@ function stubChain(onGetSlot?: () => void): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(stubChain()).generateUnsignedCreateLookupTable({ + return new CreateLookupTable().generate(stubChain(), { tokenAddress: TOKEN, poolProgramAddress: POOL_PROGRAM, payer: PAYER, @@ -73,9 +81,7 @@ describe('CreateLookupTable (cct/solana)', () => { }) it('accepts a canonical pool type', async () => { - const unsigned = await SolanaTokenManager.fromChain( - stubChain(), - ).generateUnsignedCreateLookupTable({ + const unsigned = await new CreateLookupTable().generate(stubChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -90,9 +96,7 @@ describe('CreateLookupTable (cct/solana)', () => { }) it('builds create-only ALT instruction in createEmpty mode', async () => { - const unsigned = await SolanaTokenManager.fromChain( - stubChain(), - ).generateUnsignedCreateLookupTable({ + const unsigned = await new CreateLookupTable().generate(stubChain(), { payer: PAYER, authority: AUTHORITY, mode: 'createEmpty', @@ -113,9 +117,7 @@ describe('CreateLookupTable (cct/solana)', () => { }) it('defaults createEmpty authority to payer', async () => { - const unsigned = await SolanaTokenManager.fromChain( - stubChain(), - ).generateUnsignedCreateLookupTable({ + const unsigned = await new CreateLookupTable().generate(stubChain(), { payer: PAYER, mode: 'createEmpty', }) @@ -158,14 +160,15 @@ describe('CreateLookupTable (cct/solana)', () => { let getSlotCalls = 0 await assert.rejects( - SolanaTokenManager.fromChain( + new CreateLookupTable().generate( stubChain(() => getSlotCalls++), - ).generateUnsignedCreateLookupTable({ - tokenAddress: TOKEN, - poolType: 'burn-mint', - poolProgramAddress: POOL_PROGRAM, - payer: PAYER, - } as never), + { + tokenAddress: TOKEN, + poolType: 'burn-mint', + poolProgramAddress: POOL_PROGRAM, + payer: PAYER, + } as never, + ), CCTParamsInvalidError, ) @@ -174,14 +177,25 @@ describe('CreateLookupTable (cct/solana)', () => { }) describe('execute', () => { + it('signs, submits, and returns the lookup table address', async () => { + const result = await new CreateLookupTable().execute(stubChain(), { + tokenAddress: TOKEN, + poolProgramAddress: POOL_PROGRAM, + wallet: WALLET, + }) + + assert.equal(result.hash, HASH) + assert.match(result.lookupTableAddress, /^[1-9A-HJ-NP-Za-km-z]+$/) + }) + it('rejects signed create+extend when authority is not the wallet', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain()).createLookupTable({ + new CreateLookupTable().execute(stubChain(), { tokenAddress: TOKEN, poolProgramAddress: POOL_PROGRAM, wallet: WALLET, - authority: AUTHORITY, + authority: PAYER, }), (err: unknown) => err instanceof CCTParamsInvalidError && 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 index 23756b46a..de85c018b 100644 --- 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 @@ -10,8 +10,8 @@ import { } 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' +import { GetTokenAdminRegistry } from './get-token-admin-registry.ts' const ROUTER = Keypair.generate().publicKey const TOKEN = Keypair.generate().publicKey @@ -59,7 +59,7 @@ function stubChain(account: { data: Buffer } | null = registryAccount()): Solana describe('GetTokenAdminRegistry (cct/solana)', () => { describe('query', () => { it('returns configured administrators, lookup table, and writable indexes', async () => { - const config = await SolanaTokenManager.fromChain(stubChain()).getTokenAdminRegistry({ + const config = await new GetTokenAdminRegistry().query(stubChain(), { address: ROUTER.toBase58(), tokenAddress: TOKEN.toBase58(), }) @@ -76,9 +76,10 @@ describe('GetTokenAdminRegistry (cct/solana)', () => { }) it('omits optional fields when unset', async () => { - const config = await SolanaTokenManager.fromChain( + const config = await new GetTokenAdminRegistry().query( stubChain(registryAccount(PublicKey.default, PublicKey.default, false, false)), - ).getTokenAdminRegistry({ address: ROUTER.toBase58(), tokenAddress: TOKEN.toBase58() }) + { address: ROUTER.toBase58(), tokenAddress: TOKEN.toBase58() }, + ) assert.deepEqual(config, { mint: TOKEN.toBase58(), @@ -89,17 +90,19 @@ describe('GetTokenAdminRegistry (cct/solana)', () => { }) it('returns disabled auto derivation setting', async () => { - const config = await SolanaTokenManager.fromChain( + const config = await new GetTokenAdminRegistry().query( stubChain(registryAccount(PENDING_ADMINISTRATOR, LOOKUP_TABLE, false)), - ).getTokenAdminRegistry({ address: ROUTER.toBase58(), tokenAddress: TOKEN.toBase58() }) + { 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( + const config = await new GetTokenAdminRegistry().query( stubChain(registryAccount(SystemProgram.programId)), - ).getTokenAdminRegistry({ address: ROUTER.toBase58(), tokenAddress: TOKEN.toBase58() }) + { address: ROUTER.toBase58(), tokenAddress: TOKEN.toBase58() }, + ) assert.equal(config.pendingAdministrator, undefined) }) @@ -107,7 +110,7 @@ describe('GetTokenAdminRegistry (cct/solana)', () => { it('rejects malformed registry data', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain({ data: Buffer.alloc(8) })).getTokenAdminRegistry({ + new GetTokenAdminRegistry().query(stubChain({ data: Buffer.alloc(8) }), { address: ROUTER.toBase58(), tokenAddress: TOKEN.toBase58(), }), @@ -118,7 +121,7 @@ describe('GetTokenAdminRegistry (cct/solana)', () => { it('rejects unregistered tokens', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain(null)).getTokenAdminRegistry({ + new GetTokenAdminRegistry().query(stubChain(null), { address: ROUTER.toBase58(), tokenAddress: TOKEN.toBase58(), }), @@ -131,7 +134,7 @@ describe('GetTokenAdminRegistry (cct/solana)', () => { it('rejects an invalid router address', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain()).getTokenAdminRegistry({ + new GetTokenAdminRegistry().query(stubChain(), { address: 'invalid', tokenAddress: TOKEN.toBase58(), }), @@ -143,7 +146,7 @@ describe('GetTokenAdminRegistry (cct/solana)', () => { it('rejects an invalid token address', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain()).getTokenAdminRegistry({ + new GetTokenAdminRegistry().query(stubChain(), { address: ROUTER.toBase58(), tokenAddress: 'invalid', }), 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 index 303f5e54b..0437d089f 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -3,13 +3,6 @@ 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 { RegisterAdmin } from './register-admin.ts' -export type { - ExecuteRegisterAdminParams, - ExecuteRegisterAdminResult, - GenerateRegisterAdminParams, - GenerateRegisterAdminResult, - RegisterAdminMethod, -} from './register-admin.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 index 5ffdb70a2..053000883 100644 --- 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 @@ -8,8 +8,8 @@ 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' +import { RegisterAdmin } from './register-admin.ts' const TOKEN = Keypair.generate().publicKey const MINT_AUTHORITY = Keypair.generate().publicKey @@ -20,6 +20,11 @@ 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 HASH = Keypair.generate().publicKey.toBase58() +const SUBMIT_WALLET = { + publicKey: MINT_AUTHORITY, + signTransaction: async (tx: T) => tx, +} const WALLET = { publicKey: Keypair.generate().publicKey, signTransaction: async (tx: T) => tx, @@ -64,15 +69,20 @@ function stubChain( context: { slot: 0 }, value: await getAccountInfo(address), }), + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), }, getTokenAdminRegistryFor: async () => ROUTER, } as unknown as SolanaChain } function generate(opts = {}, registered = false, mintAuthority: PublicKey | null = MINT_AUTHORITY) { - return SolanaTokenManager.fromChain( - stubChain(registered, mintAuthority), - ).generateUnsignedRegisterAdmin({ + return new RegisterAdmin().generate(stubChain(registered, mintAuthority), { tokenAddress: TOKEN.toBase58(), address: ADDRESS, payer: PAYER, @@ -131,6 +141,19 @@ describe('RegisterAdmin (cct/solana)', () => { ) }) + it('rejects owner registration without a mint authority', async () => { + await assert.rejects( + () => + generate( + { registrationMethod: 'owner', administrator: ADMINISTRATOR.toBase58() }, + false, + null, + ), + (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( () => @@ -147,9 +170,7 @@ describe('RegisterAdmin (cct/solana)', () => { it('rejects a missing Router config with a typed error', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain( - stubChain(false, MINT_AUTHORITY, false), - ).generateUnsignedRegisterAdmin({ + new RegisterAdmin().generate(stubChain(false, MINT_AUTHORITY, false), { tokenAddress: TOKEN.toBase58(), address: ADDRESS, registrationMethod: 'ccip-admin', @@ -177,10 +198,22 @@ describe('RegisterAdmin (cct/solana)', () => { }) describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + assert.deepEqual( + await new RegisterAdmin().execute(stubChain(), { + tokenAddress: TOKEN.toBase58(), + address: ADDRESS, + registrationMethod: 'owner', + wallet: SUBMIT_WALLET, + }), + { hash: HASH }, + ) + }) + it('rejects an authority that differs from the executing wallet', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain()).registerAdmin({ + new RegisterAdmin().execute(stubChain(), { tokenAddress: TOKEN.toBase58(), address: ADDRESS, registrationMethod: 'owner', 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 index f2bf50c0b..f9cfd4cd5 100644 --- 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 @@ -7,7 +7,8 @@ import { CCIPWalletInvalidError } from '../../../../errors/index.ts' import { ChainFamily } from '../../../../networks.ts' import type { SolanaChain } from '../../../../solana/index.ts' import { CCTParamsInvalidError } from '../../../errors.ts' -import { DEFAULT_WRITABLE_INDEXES, SolanaTokenManager } from '../../index.ts' +import { DEFAULT_WRITABLE_INDEXES } from '../constants.ts' +import { SetPool } from './set-pool.ts' const BLOCKHASH = PublicKey.default.toBase58() const TOKEN = Keypair.generate().publicKey.toBase58() @@ -32,7 +33,7 @@ function stubChain(router = ROUTER, onAddress?: (address: string) => void): Sola } function generate(opts = {}) { - return SolanaTokenManager.fromChain(stubChain()).generateUnsignedSetPool({ + return new SetPool().generate(stubChain(), { tokenAddress: TOKEN, address: ADDRESS, poolLookupTableAddress: POOL_LOOKUP_TABLE, @@ -75,17 +76,16 @@ describe('SetPool (cct/solana)', () => { it('resolves the router from address', async () => { let requestedAddress: string | undefined - const cct = SolanaTokenManager.fromChain( + const unsigned = await new SetPool().generate( stubChain(ROUTER, (address) => (requestedAddress = address)), + { + tokenAddress: TOKEN, + address: ADDRESS, + poolLookupTableAddress: POOL_LOOKUP_TABLE, + payer: PAYER, + }, ) - 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) }) @@ -102,15 +102,16 @@ describe('SetPool (cct/solana)', () => { let routerLookups = 0 await assert.rejects( - SolanaTokenManager.fromChain( + new SetPool().generate( stubChain(ROUTER, () => routerLookups++), - ).generateUnsignedSetPool({ - tokenAddress: TOKEN, - address: ADDRESS, - poolLookupTableAddress: POOL_LOOKUP_TABLE, - payer: PAYER, - writableIndexes: [], - }), + { + tokenAddress: TOKEN, + address: ADDRESS, + poolLookupTableAddress: POOL_LOOKUP_TABLE, + payer: PAYER, + writableIndexes: [], + }, + ), CCTParamsInvalidError, ) @@ -121,7 +122,7 @@ describe('SetPool (cct/solana)', () => { describe('execute', () => { it('rejects an invalid wallet before generating instructions', async () => { await assert.rejects( - SolanaTokenManager.fromChain(stubChain()).setPool({ + new SetPool().execute(stubChain(), { tokenAddress: TOKEN, address: ADDRESS, poolLookupTableAddress: POOL_LOOKUP_TABLE, 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 index 345d85580..ea36b3ca5 100644 --- 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 @@ -6,9 +6,8 @@ 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' -import type { GenerateTransferAdminParams } from './transfer-admin.ts' +import { type GenerateTransferAdminParams, TransferAdmin } from './transfer-admin.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const ADDRESS = Keypair.generate().publicKey.toBase58() @@ -16,6 +15,11 @@ 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 HASH = Keypair.generate().publicKey.toBase58() +const SUBMIT_WALLET = { + publicKey: new PublicKey(CURRENT_ADMIN), + signTransaction: async (tx: T) => tx, +} const WALLET = { publicKey: Keypair.generate().publicKey, signTransaction: async (tx: T) => tx, @@ -28,7 +32,15 @@ function stubChain( ): SolanaChain { return { logger: { debug() {}, info() {}, warn() {}, error() {} }, - connection: {}, + 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 } }), + }, getTokenAdminRegistryFor: async (address: string) => { onAddress?.(address) return ROUTER @@ -38,7 +50,7 @@ function stubChain( } function generate(opts: Partial = {}) { - return SolanaTokenManager.fromChain(stubChain()).generateUnsignedTransferAdmin({ + return new TransferAdmin().generate(stubChain(), { tokenAddress: TOKEN, address: ADDRESS, newAdmin: NEW_ADMIN, @@ -88,18 +100,17 @@ describe('TransferAdmin (cct/solana)', () => { it('resolves the router from address', async () => { let requestedAddress: string | undefined - const cct = SolanaTokenManager.fromChain( + await new TransferAdmin().generate( stubChain(CURRENT_ADMIN, (address) => (requestedAddress = address)), + { + tokenAddress: TOKEN, + address: ADDRESS, + newAdmin: NEW_ADMIN, + payer: PAYER, + authority: CURRENT_ADMIN, + }, ) - await cct.generateUnsignedTransferAdmin({ - tokenAddress: TOKEN, - address: ADDRESS, - newAdmin: NEW_ADMIN, - payer: PAYER, - authority: CURRENT_ADMIN, - }) - assert.equal(requestedAddress, ADDRESS) }) }) @@ -113,19 +124,18 @@ describe('TransferAdmin (cct/solana)', () => { }) 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, - }), + new TransferAdmin().generate( + stubChain(PublicKey.default.toBase58(), undefined, CURRENT_ADMIN), + { + tokenAddress: TOKEN, + address: ADDRESS, + newAdmin: NEW_ADMIN, + payer: PAYER, + authority: CURRENT_ADMIN, + }, + ), (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'authority' && @@ -136,10 +146,22 @@ describe('TransferAdmin (cct/solana)', () => { }) describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + assert.deepEqual( + await new TransferAdmin().execute(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + newAdmin: NEW_ADMIN, + wallet: SUBMIT_WALLET, + }), + { hash: HASH }, + ) + }) + it('requires the current admin to be the executing wallet', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain()).transferAdmin({ + new TransferAdmin().execute(stubChain(), { tokenAddress: TOKEN, address: ADDRESS, newAdmin: NEW_ADMIN, 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 index 70468f538..4796f528a 100644 --- 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 @@ -8,8 +8,8 @@ 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' +import { AcceptOwnership } from './accept-ownership.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -71,7 +71,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedAcceptOwnership({ + return new AcceptOwnership().generate(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -112,9 +112,7 @@ describe('AcceptOwnership (cct/solana)', () => { }) it('defaults authority to payer', async () => { - const unsigned = await SolanaTokenManager.fromChain( - chain(PAYER), - ).generateUnsignedAcceptOwnership({ + const unsigned = await new AcceptOwnership().generate(chain(PAYER), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -143,11 +141,9 @@ describe('AcceptOwnership (cct/solana)', () => { }) it('rejects when there is no proposed owner', async () => { - const cct = SolanaTokenManager.fromChain(chain(PublicKey.default.toBase58())) - await assert.rejects( () => - cct.generateUnsignedAcceptOwnership({ + new AcceptOwnership().generate(chain(PublicKey.default.toBase58()), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -174,7 +170,7 @@ describe('AcceptOwnership (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).acceptOwnership({ + const result = await new AcceptOwnership().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', wallet: WALLET, @@ -186,7 +182,7 @@ describe('AcceptOwnership (cct/solana)', () => { it('rejects a non-wallet authority for signed acceptance', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).acceptOwnership({ + new AcceptOwnership().execute(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', authority: AUTHORITY, 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 index 04ccfb482..629be5e87 100644 --- 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 @@ -7,12 +7,12 @@ 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' +import { AppendRemotePoolAddresses } from './append-remote-pool-addresses.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -47,7 +47,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedAppendRemotePoolAddresses({ + return new AppendRemotePoolAddresses().generate(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -139,7 +139,7 @@ describe('AppendRemotePoolAddresses (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).appendRemotePoolAddresses({ + const result = await new AppendRemotePoolAddresses().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', remoteChainSelector: SELECTOR, @@ -153,7 +153,7 @@ describe('AppendRemotePoolAddresses (cct/solana)', () => { it('rejects a non-wallet authority for signed appending', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).appendRemotePoolAddresses({ + new AppendRemotePoolAddresses().execute(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', authority: AUTHORITY, 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 index b25dbbdc4..15fadf121 100644 --- 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 @@ -8,8 +8,8 @@ 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' +import { ApplyChainUpdates } from './apply-chain-updates.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -54,7 +54,7 @@ function batchChains() { } function generateBatches(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedApplyChainUpdates({ + return new ApplyChainUpdates().generateBatch(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -81,6 +81,35 @@ async function generate(opts = {}) { describe('ApplyChainUpdates (cct/solana)', () => { describe('generate', () => { + it('requires the batch API', async () => { + assert.throws( + () => new ApplyChainUpdates().generate(chain(), {} as never), + (error: unknown) => CCIPError.isCCIPError(error) && error.code === 'METHOD_UNSUPPORTED', + ) + assert.throws( + () => new ApplyChainUpdates().execute(chain(), {} as never), + (error: unknown) => CCIPError.isCCIPError(error) && error.code === 'METHOD_UNSUPPORTED', + ) + }) + + it('builds a single unsigned transaction internally', async () => { + const operation = new ApplyChainUpdates() + const params = { + tokenAddress: TOKEN, + poolType: 'burn-mint' as const, + payer: PAYER, + authority: AUTHORITY, + remoteChainSelectorsToRemove: [SELECTOR], + chainsToAdd: [], + } + const unsigned = await (operation as any).buildUnsigned( + chain(), + (operation as any).prepare(params), + ) + + assert.equal(unsigned.instructions.length, 1) + }) + it('builds delete, initialize, edit, and rate-limit instructions', async () => { const unsigned = await generate() const poolProgram = resolveTokenPoolProgram('burn-mint') @@ -155,16 +184,14 @@ describe('ApplyChainUpdates (cct/solana)', () => { }) 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(), - }, - ) + const batches = await new ApplyChainUpdates().generateBatch(chain(), { + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + remoteChainSelectorsToRemove: [], + chainsToAdd: batchChains(), + }) assert.equal(batches.length, 2) assert.deepEqual( @@ -191,7 +218,7 @@ describe('ApplyChainUpdates (cct/solana)', () => { it('rejects a chain update that cannot fit one transaction', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).generateUnsignedApplyChainUpdates({ + new ApplyChainUpdates().generateBatch(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -340,7 +367,7 @@ describe('ApplyChainUpdates (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns all tx hashes', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).applyChainUpdates({ + const result = await new ApplyChainUpdates().executeBatch(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', remoteChainSelectorsToRemove: [SELECTOR], @@ -361,7 +388,7 @@ describe('ApplyChainUpdates (cct/solana)', () => { }) it('submits every safely packed batch and returns all hashes', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).applyChainUpdates({ + const result = await new ApplyChainUpdates().executeBatch(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', remoteChainSelectorsToRemove: [], @@ -384,7 +411,7 @@ describe('ApplyChainUpdates (cct/solana)', () => { await assert.rejects( () => - SolanaTokenManager.fromChain(failedChain).applyChainUpdates({ + new ApplyChainUpdates().executeBatch(failedChain, { tokenAddress: TOKEN, poolType: 'burn-mint', remoteChainSelectorsToRemove: [], @@ -405,7 +432,7 @@ describe('ApplyChainUpdates (cct/solana)', () => { it('rejects a non-wallet authority for signed configuration', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).applyChainUpdates({ + new ApplyChainUpdates().executeBatch(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', authority: AUTHORITY, 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 index ec04b1953..82eb3a6c7 100644 --- 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 @@ -7,8 +7,8 @@ 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' +import { ConfigureAllowlist } from './configure-allowlist.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -43,7 +43,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(stubChain()).generateUnsignedConfigureAllowlist({ + return new ConfigureAllowlist().generate(stubChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -109,9 +109,7 @@ describe('ConfigureAllowlist (cct/solana)', () => { it('uses a compatible custom pool program', async () => { const poolProgramAddress = Keypair.generate().publicKey.toBase58() - const unsigned = await SolanaTokenManager.fromChain( - stubChain(), - ).generateUnsignedConfigureAllowlist({ + const unsigned = await new ConfigureAllowlist().generate(stubChain(), { tokenAddress: TOKEN, poolProgramAddress, payer: PAYER, @@ -177,8 +175,20 @@ describe('ConfigureAllowlist (cct/solana)', () => { }) describe('execute', () => { + it('rejects an invalid wallet', async () => { + await assert.rejects(() => + new ConfigureAllowlist().execute(stubChain(), { + tokenAddress: TOKEN, + poolType: 'burn-mint', + add: [ALLOWED], + enabled: true, + wallet: {} as never, + }), + ) + }) + it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).configureAllowlist({ + const result = await new ConfigureAllowlist().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', add: [ALLOWED], @@ -192,7 +202,7 @@ describe('ConfigureAllowlist (cct/solana)', () => { it('rejects a non-wallet authority for signed configuration', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain()).configureAllowlist({ + new ConfigureAllowlist().execute(stubChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', authority: AUTHORITY, 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 index 02c53625b..2206615c8 100644 --- 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 @@ -7,13 +7,14 @@ 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' +import { CreateTokenMultisig } from './create-token-multisig.ts' const PAYER = Keypair.generate().publicKey.toBase58() const AUTHORITY = Keypair.generate().publicKey.toBase58() const MINT = Keypair.generate().publicKey.toBase58() const POOL_PROGRAM = '41FGToCmdaWa1dgZLKFAjvmx6e6AjVTX7SVRibvsMGVB' +const HASH = Keypair.generate().publicKey.toBase58() const WALLET = { publicKey: Keypair.generate().publicKey, signTransaction: async (tx: T) => tx, @@ -55,7 +56,7 @@ function stubChain(mintAuthority?: PublicKey | null): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(stubChain()).generateUnsignedCreateTokenMultisig({ + return new CreateTokenMultisig().generate(stubChain(), { tokenAddress: MINT, poolType: 'burn-mint', threshold: 2, @@ -107,9 +108,45 @@ describe('CreateTokenMultisig (cct/solana)', () => { assert.ok(unsigned.instructions[1]!.keys.some((key) => key.pubkey.equals(signer))) }) + + it('generates a seed when none is supplied', async () => { + assert.ok((await generate({ threshold: 1, seed: undefined })).multisigAddress) + }) + + it('deduplicates a signer that matches the mint authority', async () => { + const unsigned = await generate({ threshold: 1, additionalSigners: [AUTHORITY] }) + + assert.equal( + unsigned.instructions[1]!.keys.filter((key) => key.pubkey.equals(new PublicKey(AUTHORITY))) + .length, + 1, + ) + }) }) describe('validation', () => { + it('rejects non-array additional signers', async () => { + await assert.rejects( + () => generate({ additionalSigners: 'not-an-array' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'additionalSigners', + ) + }) + + it('rejects too many multisig signers', async () => { + await assert.rejects( + () => + generate({ + threshold: 1, + additionalSigners: Array.from({ length: 10 }, () => + Keypair.generate().publicKey.toBase58(), + ), + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'additionalSigners', + ) + }) + it('rejects invalid pool type', async () => { await assert.rejects( () => generate({ poolType: 'custom' }), @@ -133,7 +170,7 @@ describe('CreateTokenMultisig (cct/solana)', () => { it('rejects mint without mint authority', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain(null)).generateUnsignedCreateTokenMultisig({ + new CreateTokenMultisig().generate(stubChain(null), { tokenAddress: MINT, poolType: 'burn-mint', threshold: 2, @@ -148,10 +185,42 @@ describe('CreateTokenMultisig (cct/solana)', () => { }) describe('execute', () => { + it('signs, submits, and returns the multisig address', async () => { + const result = await new CreateTokenMultisig().execute( + Object.assign(stubChain(new PublicKey(AUTHORITY)), { + connection: { + getAccountInfo: async () => ({ + owner: TOKEN_PROGRAM_ID, + data: mintData(new PublicKey(AUTHORITY)), + executable: false, + lamports: 1, + }), + getMinimumBalanceForRentExemption: async () => 123, + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + }), + { + tokenAddress: MINT, + poolType: 'burn-mint', + threshold: 1, + wallet: { ...WALLET, publicKey: new PublicKey(AUTHORITY) }, + }, + ) + + assert.equal(result.hash, HASH) + assert.ok(result.multisigAddress) + }) + it('rejects signed execute when wallet is not mint authority', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain()).createTokenMultisig({ + new CreateTokenMultisig().execute(stubChain(), { tokenAddress: MINT, poolType: 'burn-mint', threshold: 2, 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 index 35ee24c89..068efdec6 100644 --- 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 @@ -7,12 +7,12 @@ 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' +import { DeleteChainRemoteConfig } from './delete-chain-remote-config.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -46,7 +46,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedDeleteChainRemoteConfig({ + return new DeleteChainRemoteConfig().generate(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -127,7 +127,7 @@ describe('DeleteChainRemoteConfig (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).deleteChainRemoteConfig({ + const result = await new DeleteChainRemoteConfig().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', remoteChainSelector: SELECTOR, @@ -140,7 +140,7 @@ describe('DeleteChainRemoteConfig (cct/solana)', () => { it('rejects a non-wallet authority for signed deletion', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).deleteChainRemoteConfig({ + new DeleteChainRemoteConfig().execute(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', authority: AUTHORITY, 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 index 2c970ae30..d45ba8b87 100644 --- 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 @@ -6,18 +6,16 @@ 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 { deriveTokenPoolSignerPda, resolveTokenPoolProgram } from '../../index.ts' import { deriveTokenPoolConfigPda } from '../../programs/token-pool.ts' +import { DeployTokenPool } from './deploy-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 HASH = Keypair.generate().publicKey.toBase58() const WALLET = { publicKey: Keypair.generate().publicKey, signTransaction: async (tx: T) => tx, @@ -31,7 +29,7 @@ function stubChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(stubChain()).generateUnsignedDeployTokenPool({ + return new DeployTokenPool().generate(stubChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -88,6 +86,13 @@ describe('DeployTokenPool (cct/solana)', () => { }) describe('validation', () => { + it('rejects a non-array allowlist', async () => { + await assert.rejects( + () => generate({ allowlist: 'not-an-array' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'allowlist', + ) + }) + it('rejects invalid pool types', async () => { await assert.rejects( () => generate({ poolType: 'custom' }), @@ -120,10 +125,35 @@ describe('DeployTokenPool (cct/solana)', () => { }) describe('execute', () => { + it('signs, submits, and returns pool addresses', async () => { + const result = await new DeployTokenPool().execute( + Object.assign(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 } }), + }, + }), + { + tokenAddress: TOKEN, + poolType: 'burn-mint', + wallet: { ...WALLET, publicKey: new PublicKey(PAYER) }, + }, + ) + + assert.equal(result.hash, HASH) + assert.ok(result.poolAddress) + assert.ok(result.poolSignerAddress) + }) + it('rejects signed deploy when authority is not the wallet', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain()).deployTokenPool({ + new DeployTokenPool().execute(stubChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', wallet: WALLET, 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 index 5b652f3d8..c48a13f89 100644 --- 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 @@ -7,12 +7,12 @@ 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' +import { EditChainRemoteConfig } from './edit-chain-remote-config.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -48,7 +48,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedEditChainRemoteConfig({ + return new EditChainRemoteConfig().generate(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -153,7 +153,7 @@ describe('EditChainRemoteConfig (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).editChainRemoteConfig({ + const result = await new EditChainRemoteConfig().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', remoteChainSelector: SELECTOR, @@ -169,7 +169,7 @@ describe('EditChainRemoteConfig (cct/solana)', () => { it('rejects a non-wallet authority for signed editing', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).editChainRemoteConfig({ + new EditChainRemoteConfig().execute(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', authority: AUTHORITY, 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 index 83dabc7cf..321f171f8 100644 --- 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 @@ -6,8 +6,8 @@ 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' +import { GetTokenPoolRemotes } from './get-token-pool-remotes.ts' function key(byte: number): PublicKey { return new PublicKey(Uint8Array.from({ length: 32 }, () => byte)) @@ -39,7 +39,7 @@ describe('GetTokenPoolRemotes (cct/solana)', () => { describe('query', () => { it('delegates selected remote config decoding to the chain reader', async () => { - const remotes = await SolanaTokenManager.fromChain(chain()).getTokenPoolRemotes({ + const remotes = await new GetTokenPoolRemotes().query(chain(), { tokenAddress: mint.toBase58(), poolProgramAddress: program.toBase58(), remoteChainSelector: selector, @@ -56,7 +56,7 @@ describe('GetTokenPoolRemotes (cct/solana)', () => { }, } as unknown as SolanaChain - const remotes = await SolanaTokenManager.fromChain(chainWithAll).getTokenPoolRemotes({ + const remotes = await new GetTokenPoolRemotes().query(chainWithAll, { tokenAddress: mint.toBase58(), poolProgramAddress: program.toBase58(), }) @@ -75,7 +75,7 @@ describe('GetTokenPoolRemotes (cct/solana)', () => { ] for (const [opts, param] of cases) { await assert.rejects( - SolanaTokenManager.fromChain(chain()).getTokenPoolRemotes({ + new GetTokenPoolRemotes().query(chain(), { tokenAddress: mint.toBase58(), poolProgramAddress: program.toBase58(), remoteChainSelector: selector, 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 index 3b11b4e6f..35de57cca 100644 --- 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 @@ -7,12 +7,12 @@ 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' +import { InitChainRemoteConfig } from './init-chain-remote-config.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -47,7 +47,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedInitChainRemoteConfig({ + return new InitChainRemoteConfig().generate(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -144,7 +144,7 @@ describe('InitChainRemoteConfig (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).initChainRemoteConfig({ + const result = await new InitChainRemoteConfig().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', remoteChainSelector: SELECTOR, @@ -159,7 +159,7 @@ describe('InitChainRemoteConfig (cct/solana)', () => { it('rejects a non-wallet authority for signed initialization', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).initChainRemoteConfig({ + new InitChainRemoteConfig().execute(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', authority: AUTHORITY, diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.test.ts index 5987ddae8..8fd45334e 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.test.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.test.ts @@ -9,12 +9,13 @@ import { ChainFamily } from '../../../../networks.ts' import { lockReleaseTokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' import type { SolanaChain } from '../../../../solana/index.ts' import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' -import { SolanaTokenManager } from '../../index.ts' import { deriveTokenPoolConfigPda, deriveTokenPoolSignerPda, resolveTokenPoolProgram, } from '../../programs/token-pool.ts' +import { ApproveToken } from '../../token/operations/approve-token.ts' +import { ProvideLiquidity } from './provide-liquidity.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -113,7 +114,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedProvideLiquidity({ + return new ProvideLiquidity().generate(chain(), { tokenAddress: TOKEN, poolType: 'lock-release', payer: PAYER, @@ -186,7 +187,7 @@ describe('ProvideLiquidity (cct/solana)', () => { ] as const) { await assert.rejects( () => - SolanaTokenManager.fromChain(pool).generateUnsignedProvideLiquidity({ + new ProvideLiquidity().generate(pool, { tokenAddress: TOKEN, poolType: 'lock-release', payer: PAYER, @@ -199,14 +200,15 @@ describe('ProvideLiquidity (cct/solana)', () => { }) it('defaults authority to payer', async () => { - const unsigned = await SolanaTokenManager.fromChain( + const unsigned = await new ProvideLiquidity().generate( chain(resolveTokenPoolProgram('lock-release'), new PublicKey(PAYER)), - ).generateUnsignedProvideLiquidity({ - tokenAddress: TOKEN, - poolType: 'lock-release', - payer: PAYER, - amount: 1_000_000n, - }) + { + tokenAddress: TOKEN, + poolType: 'lock-release', + payer: PAYER, + amount: 1_000_000n, + }, + ) assert.equal(unsigned.instructions[0]!.keys[6]!.pubkey.toBase58(), PAYER) }) @@ -214,7 +216,7 @@ describe('ProvideLiquidity (cct/solana)', () => { it('uses a source account that can be delegated to the pool signer', async () => { const poolProgram = resolveTokenPoolProgram('lock-release') const poolSigner = deriveTokenPoolSignerPda(poolProgram, new PublicKey(TOKEN)) - const approval = await SolanaTokenManager.fromChain(chain()).generateUnsignedApproveToken({ + const approval = await new ApproveToken().generate(chain(), { payer: AUTHORITY, tokenAddress: TOKEN, delegate: poolSigner.toBase58(), @@ -232,15 +234,16 @@ describe('ProvideLiquidity (cct/solana)', () => { it('supports a compatible custom pool program', async () => { const poolProgramAddress = Keypair.generate().publicKey.toBase58() - const unsigned = await SolanaTokenManager.fromChain( + const unsigned = await new ProvideLiquidity().generate( chain(new PublicKey(poolProgramAddress)), - ).generateUnsignedProvideLiquidity({ - tokenAddress: TOKEN, - poolProgramAddress, - payer: PAYER, - authority: AUTHORITY, - amount: 1_000_000n, - }) + { + tokenAddress: TOKEN, + poolProgramAddress, + payer: PAYER, + authority: AUTHORITY, + amount: 1_000_000n, + }, + ) assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) }) @@ -272,7 +275,7 @@ describe('ProvideLiquidity (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).provideLiquidity({ + const result = await new ProvideLiquidity().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'lock-release', amount: 1_000_000n, @@ -285,7 +288,7 @@ describe('ProvideLiquidity (cct/solana)', () => { it('rejects a non-wallet authority for signed liquidity provision', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).provideLiquidity({ + new ProvideLiquidity().execute(chain(), { tokenAddress: TOKEN, poolType: 'lock-release', amount: 1_000_000n, diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts index fefb7708b..574a5fbbc 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts @@ -114,7 +114,9 @@ export class ProvideLiquidity extends SolanaOperation< if (remoteTokenAccountInfo.amount < opts.amount) throw new CCTTxFailedError( this.name, - `source token account ${remoteTokenAccount.toBase58()} has ${remoteTokenAccountInfo.amount}, but ${opts.amount} is required; mint or transfer tokens first`, + `source token account ${remoteTokenAccount.toBase58()} has ${ + remoteTokenAccountInfo.amount + }, but ${opts.amount} is required; mint or transfer tokens first`, ) // The pool signer transfers from the rebalancer ATA as its SPL Token delegate. @@ -147,9 +149,17 @@ export class ProvideLiquidity extends SolanaOperation< .instruction() chain.logger.debug( - `${this.name}: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}, amount = ${opts.amount}`, + `${ + this.name + }: token = ${opts.tokenAddress.toBase58()}, poolProgram = ${opts.poolProgram.toBase58()}, amount = ${ + opts.amount + }`, ) - return { family: ChainFamily.Solana, instructions: [instruction], mainIndex: 0 } + return { + family: ChainFamily.Solana, + instructions: [instruction], + mainIndex: 0, + } } /** Generate, sign, simulate, send, and confirm with the rebalancer wallet. */ 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 index 2ac229e97..f0a1e2415 100644 --- 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 @@ -8,8 +8,8 @@ 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' +import { RemoveFromAllowlist } from './remove-from-allowlist.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -44,7 +44,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(stubChain()).generateUnsignedRemoveFromAllowlist({ + return new RemoveFromAllowlist().generate(stubChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -102,9 +102,7 @@ describe('RemoveFromAllowlist (cct/solana)', () => { it('uses a compatible custom pool program', async () => { const poolProgramAddress = Keypair.generate().publicKey.toBase58() - const unsigned = await SolanaTokenManager.fromChain( - stubChain(), - ).generateUnsignedRemoveFromAllowlist({ + const unsigned = await new RemoveFromAllowlist().generate(stubChain(), { tokenAddress: TOKEN, poolProgramAddress, payer: PAYER, @@ -162,7 +160,7 @@ describe('RemoveFromAllowlist (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).removeFromAllowlist({ + const result = await new RemoveFromAllowlist().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', remove: [ALLOWED], @@ -175,7 +173,7 @@ describe('RemoveFromAllowlist (cct/solana)', () => { it('rejects a non-wallet authority for signed removal', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(stubChain()).removeFromAllowlist({ + new RemoveFromAllowlist().execute(stubChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', authority: AUTHORITY, diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.test.ts index a92451c21..8653cf1ee 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.test.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/set-can-accept-liquidity.test.ts @@ -7,8 +7,8 @@ import { ChainFamily } from '../../../../networks.ts' import { lockReleaseTokenPoolCoder } 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' +import { SetCanAcceptLiquidity } from './set-can-accept-liquidity.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -42,7 +42,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedSetCanAcceptLiquidity({ + return new SetCanAcceptLiquidity().generate(chain(), { tokenAddress: TOKEN, poolType: 'lock-release', payer: PAYER, @@ -122,7 +122,7 @@ describe('SetCanAcceptLiquidity (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).setCanAcceptLiquidity({ + const result = await new SetCanAcceptLiquidity().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'lock-release', allow: ALLOW, @@ -135,7 +135,7 @@ describe('SetCanAcceptLiquidity (cct/solana)', () => { it('rejects a non-wallet authority for signed configuration', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).setCanAcceptLiquidity({ + new SetCanAcceptLiquidity().execute(chain(), { tokenAddress: TOKEN, poolType: 'lock-release', allow: ALLOW, 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 index d6f3c70b8..337f5a67b 100644 --- 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 @@ -7,12 +7,12 @@ 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' +import { SetChainRateLimit } from './set-chain-rate-limit.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -46,7 +46,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedSetChainRateLimit({ + return new SetChainRateLimit().generate(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -193,7 +193,7 @@ describe('SetChainRateLimit (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).setChainRateLimit({ + const result = await new SetChainRateLimit().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', remoteChainSelector: SELECTOR, @@ -208,7 +208,7 @@ describe('SetChainRateLimit (cct/solana)', () => { it('rejects a non-wallet authority for signed configuration', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).setChainRateLimit({ + new SetChainRateLimit().execute(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', authority: AUTHORITY, 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 index 525537b4f..526e7a5cc 100644 --- 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 @@ -7,8 +7,8 @@ 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' +import { SetRateLimitAdmin } from './set-rate-limit-admin.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -42,7 +42,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedSetRateLimitAdmin({ + return new SetRateLimitAdmin().generate(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -115,7 +115,7 @@ describe('SetRateLimitAdmin (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).setRateLimitAdmin({ + const result = await new SetRateLimitAdmin().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', newRateLimitAdmin: NEW_RATE_LIMIT_ADMIN, @@ -128,7 +128,7 @@ describe('SetRateLimitAdmin (cct/solana)', () => { it('rejects a non-wallet authority for signed configuration', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).setRateLimitAdmin({ + new SetRateLimitAdmin().execute(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', newRateLimitAdmin: NEW_RATE_LIMIT_ADMIN, diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/set-rebalancer.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/set-rebalancer.test.ts index 618df4092..140cc2ac7 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/set-rebalancer.test.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/set-rebalancer.test.ts @@ -7,8 +7,8 @@ import { ChainFamily } from '../../../../networks.ts' import { lockReleaseTokenPoolCoder } 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' +import { SetRebalancer } from './set-rebalancer.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -42,7 +42,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedSetRebalancer({ + return new SetRebalancer().generate(chain(), { tokenAddress: TOKEN, poolType: 'lock-release', payer: PAYER, @@ -125,7 +125,7 @@ describe('SetRebalancer (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).setRebalancer({ + const result = await new SetRebalancer().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'lock-release', rebalancer: REBALANCER, @@ -138,7 +138,7 @@ describe('SetRebalancer (cct/solana)', () => { it('rejects a non-wallet authority for signed configuration', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).setRebalancer({ + new SetRebalancer().execute(chain(), { tokenAddress: TOKEN, poolType: 'lock-release', rebalancer: REBALANCER, 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 index 44da71121..b9177b928 100644 --- 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 @@ -8,8 +8,8 @@ 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' +import { TransferOwnership } from './transfer-ownership.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -69,7 +69,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedTransferOwnership({ + return new TransferOwnership().generate(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', payer: PAYER, @@ -156,7 +156,7 @@ describe('TransferOwnership (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).transferOwnership({ + const result = await new TransferOwnership().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'burn-mint', newOwner: NEW_OWNER, @@ -169,7 +169,7 @@ describe('TransferOwnership (cct/solana)', () => { it('rejects a non-wallet authority for signed transfer', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).transferOwnership({ + new TransferOwnership().execute(chain(), { tokenAddress: TOKEN, poolType: 'burn-mint', newOwner: NEW_OWNER, diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/withdraw-liquidity.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/withdraw-liquidity.test.ts index d2c301488..f65f582a3 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/withdraw-liquidity.test.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/withdraw-liquidity.test.ts @@ -9,12 +9,12 @@ import { ChainFamily } from '../../../../networks.ts' import { lockReleaseTokenPoolCoder } from '../../../../solana/idl/token-pool-coder.ts' import type { SolanaChain } from '../../../../solana/index.ts' import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' -import { SolanaTokenManager } from '../../index.ts' import { deriveTokenPoolConfigPda, deriveTokenPoolSignerPda, resolveTokenPoolProgram, } from '../../programs/token-pool.ts' +import { WithdrawLiquidity } from './withdraw-liquidity.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -118,7 +118,7 @@ function submitChain(): SolanaChain { } function generate(opts = {}) { - return SolanaTokenManager.fromChain(chain()).generateUnsignedWithdrawLiquidity({ + return new WithdrawLiquidity().generate(chain(), { tokenAddress: TOKEN, poolType: 'lock-release', payer: PAYER, @@ -191,7 +191,7 @@ describe('WithdrawLiquidity (cct/solana)', () => { ] as const) { await assert.rejects( () => - SolanaTokenManager.fromChain(pool).generateUnsignedWithdrawLiquidity({ + new WithdrawLiquidity().generate(pool, { tokenAddress: TOKEN, poolType: 'lock-release', payer: PAYER, @@ -204,29 +204,31 @@ describe('WithdrawLiquidity (cct/solana)', () => { }) it('defaults authority to payer', async () => { - const unsigned = await SolanaTokenManager.fromChain( + const unsigned = await new WithdrawLiquidity().generate( chain(resolveTokenPoolProgram('lock-release'), new PublicKey(PAYER)), - ).generateUnsignedWithdrawLiquidity({ - tokenAddress: TOKEN, - poolType: 'lock-release', - payer: PAYER, - amount: 1_000_000n, - }) + { + tokenAddress: TOKEN, + poolType: 'lock-release', + payer: PAYER, + amount: 1_000_000n, + }, + ) assert.equal(unsigned.instructions[0]!.keys[6]!.pubkey.toBase58(), PAYER) }) it('supports a compatible custom pool program', async () => { const poolProgramAddress = Keypair.generate().publicKey.toBase58() - const unsigned = await SolanaTokenManager.fromChain( + const unsigned = await new WithdrawLiquidity().generate( chain(new PublicKey(poolProgramAddress)), - ).generateUnsignedWithdrawLiquidity({ - tokenAddress: TOKEN, - poolProgramAddress, - payer: PAYER, - authority: AUTHORITY, - amount: 1_000_000n, - }) + { + tokenAddress: TOKEN, + poolProgramAddress, + payer: PAYER, + authority: AUTHORITY, + amount: 1_000_000n, + }, + ) assert.equal(unsigned.instructions[0]?.programId.toBase58(), poolProgramAddress) }) @@ -258,7 +260,7 @@ describe('WithdrawLiquidity (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).withdrawLiquidity({ + const result = await new WithdrawLiquidity().execute(submitChain(), { tokenAddress: TOKEN, poolType: 'lock-release', amount: 1_000_000n, @@ -271,7 +273,7 @@ describe('WithdrawLiquidity (cct/solana)', () => { it('rejects a non-wallet authority for signed liquidity withdrawal', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).withdrawLiquidity({ + new WithdrawLiquidity().execute(chain(), { tokenAddress: TOKEN, poolType: 'lock-release', amount: 1_000_000n, diff --git a/ccip-sdk/src/cct/solana/token/constants.ts b/ccip-sdk/src/cct/solana/token/constants.ts index 841d7a5ae..76061c0cb 100644 --- a/ccip-sdk/src/cct/solana/token/constants.ts +++ b/ccip-sdk/src/cct/solana/token/constants.ts @@ -1,3 +1,8 @@ +import { PublicKey } from '@solana/web3.js' + +/** Metaplex Token Metadata program address. */ +export const METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s') + /** SPL Token authority roles that can be set. */ export const TOKEN_AUTHORITY_TYPES = { MINT: 'mint', diff --git a/ccip-sdk/src/cct/solana/token/operations/approve-token.test.ts b/ccip-sdk/src/cct/solana/token/operations/approve-token.test.ts index 2058db941..3f1e2aa79 100644 --- a/ccip-sdk/src/cct/solana/token/operations/approve-token.test.ts +++ b/ccip-sdk/src/cct/solana/token/operations/approve-token.test.ts @@ -16,8 +16,8 @@ import { import { ChainFamily } from '../../../../networks.ts' import type { SolanaChain } from '../../../../solana/index.ts' import { CCTParamsInvalidError } from '../../../errors.ts' -import { SolanaTokenManager } from '../../index.ts' import { U64_MAX } from '../../validate.ts' +import { ApproveToken } from './approve-token.ts' const TOKEN = Keypair.generate().publicKey const PAYER = Keypair.generate().publicKey.toBase58() @@ -60,7 +60,7 @@ function submitChain(): SolanaChain { } function generate(opts: Record = {}, mintOwner?: PublicKey | null) { - return SolanaTokenManager.fromChain(chain(mintOwner)).generateUnsignedApproveToken({ + return new ApproveToken().generate(chain(mintOwner), { payer: PAYER, tokenAddress: TOKEN.toBase58(), delegate: DELEGATE, @@ -165,14 +165,12 @@ describe('ApproveToken (cct/solana)', () => { it('rejects a missing token account before submission', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain(TOKEN_PROGRAM_ID, false)).generateUnsignedApproveToken( - { - payer: PAYER, - tokenAddress: TOKEN.toBase58(), - delegate: DELEGATE, - amount: 1n, - }, - ), + new ApproveToken().generate(chain(TOKEN_PROGRAM_ID, false), { + payer: PAYER, + tokenAddress: TOKEN.toBase58(), + delegate: DELEGATE, + amount: 1n, + }), (err: unknown) => err instanceof CCIPTokenAccountNotFoundError, ) }) @@ -191,7 +189,7 @@ describe('ApproveToken (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).approveToken({ + const result = await new ApproveToken().execute(submitChain(), { tokenAddress: TOKEN.toBase58(), delegate: DELEGATE, amount: 1n, @@ -207,7 +205,7 @@ describe('ApproveToken (cct/solana)', () => { ]) { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).approveToken({ + new ApproveToken().execute(chain(), { tokenAddress: TOKEN.toBase58(), delegate: DELEGATE, amount: 1n, 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 index c36517b84..584fbb308 100644 --- 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 @@ -7,7 +7,7 @@ import { TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, } from '@solana/spl-token' -import { type PublicKey, Keypair } from '@solana/web3.js' +import { Keypair, PublicKey } from '@solana/web3.js' import { CCIPTokenMintInvalidError, @@ -16,11 +16,16 @@ import { } from '../../../../errors/index.ts' import { ChainFamily } from '../../../../networks.ts' import type { SolanaChain } from '../../../../solana/index.ts' -import { SolanaTokenManager } from '../../index.ts' +import { CreateTokenAccount } from './create-token-account.ts' const PAYER = Keypair.generate().publicKey.toBase58() const MINT = Keypair.generate().publicKey const OWNER = Keypair.generate().publicKey +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} function stubChain(mintOwner: PublicKey | null = TOKEN_2022_PROGRAM_ID): SolanaChain { return { @@ -31,8 +36,23 @@ function stubChain(mintOwner: PublicKey | null = TOKEN_2022_PROGRAM_ID): SolanaC } as unknown as SolanaChain } +function submitChain(): SolanaChain { + return Object.assign(stubChain(), { + connection: { + getAccountInfo: async () => ({ owner: TOKEN_2022_PROGRAM_ID }), + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ + blockhash: PublicKey.default.toBase58(), + lastValidBlockHeight: 1, + }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + }) +} + function generate(opts = {}, mintOwner?: PublicKey | null) { - return SolanaTokenManager.fromChain(stubChain(mintOwner)).generateUnsignedCreateTokenAccount({ + return new CreateTokenAccount().generate(stubChain(mintOwner), { payer: PAYER, tokenAddress: MINT.toBase58(), ownerAddress: OWNER.toBase58(), @@ -90,9 +110,27 @@ describe('CreateTokenAccount (cct/solana)', () => { }) describe('execute', () => { + it('signs, submits, and returns the token account address', async () => { + const result = await new CreateTokenAccount().execute(submitChain(), { + tokenAddress: MINT.toBase58(), + ownerAddress: OWNER.toBase58(), + wallet: WALLET, + }) + + assert.deepEqual(result, { + hash: HASH, + tokenAccountAddress: getAssociatedTokenAddressSync( + MINT, + OWNER, + true, + TOKEN_2022_PROGRAM_ID, + ).toBase58(), + }) + }) + it('rejects an invalid wallet before generating instructions', async () => { await assert.rejects( - SolanaTokenManager.fromChain(stubChain()).createTokenAccount({ + new CreateTokenAccount().execute(stubChain(), { tokenAddress: MINT.toBase58(), ownerAddress: OWNER.toBase58(), wallet: {}, 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 index b891c8664..83b789d43 100644 --- a/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts @@ -7,11 +7,16 @@ 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 { DeployToken } from './deploy-token.ts' const BLOCKHASH = PublicKey.default.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() const METAPLEX_PROGRAM = 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s' +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: Keypair.generate().publicKey, + signTransaction: async (tx: T) => tx, +} function stubChain(): SolanaChain { return { @@ -25,8 +30,21 @@ function stubChain(): SolanaChain { } as unknown as SolanaChain } -function generate(opts = {}) { - return SolanaTokenManager.fromChain(stubChain()).generateUnsignedDeployToken({ +function submitChain(): SolanaChain { + return Object.assign(stubChain(), { + connection: { + rpcEndpoint: 'http://localhost:8899', + getMinimumBalanceForRentExemption: async () => 123, + simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + getLatestBlockhash: async () => ({ blockhash: BLOCKHASH, lastValidBlockHeight: 0 }), + sendTransaction: async () => HASH, + confirmTransaction: async () => ({ value: { err: null } }), + }, + }) +} + +function generate(opts: Record = {}) { + return new DeployToken().generate(stubChain(), { decimals: 9, withMetaplex: false, payer: PAYER, @@ -53,8 +71,8 @@ describe('DeployToken (cct/solana)', () => { 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' }) + it('uses caller seed for reproducible mint address and supports no freeze authority', async () => { + const a = await generate({ seed: 'mint_seed', freezeAuthority: null }) const b = await generate({ seed: 'mint_seed' }) assert.equal(a.tokenAddress, b.tokenAddress) @@ -100,31 +118,39 @@ describe('DeployToken (cct/solana)', () => { }) }) - 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, + describe('validation', () => { + it('rejects invalid base and pre-mint parameters', async () => { + for (const [opts, param] of [ + [{ decimals: 256 }, 'decimals'], + [{ tokenProgram: 'invalid' }, 'tokenProgram'], + [{ withMetaplex: 'yes' }, 'withMetaplex'], + [{ seed: '' }, 'seed'], + [{ mintAuthority: 'invalid' }, 'mintAuthority'], + [{ freezeAuthority: 'invalid' }, 'freezeAuthority'], + [{ preMint: 0n }, 'preMint'], + [{ preMint: 1 }, 'preMint'], + [{ preMint: 1n }, 'preMintRecipient'], + [{ preMint: 1n, preMintRecipient: 'invalid' }, 'preMintRecipient'], + ] as const) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) } + }) - 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', - ) + it('rejects invalid Metaplex name and URI parameters', async () => { + for (const [opts, param] of [ + [{ withMetaplex: true, name: '', symbol: 'MTK' }, 'name'], + [{ withMetaplex: true, name: 'My Token', symbol: 'MTK', uri: 1 }, 'uri'], + ] as const) { + await assert.rejects( + () => generate(opts), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } }) - }) - describe('validation', () => { it('rejects seeds over 32 UTF-8 bytes', async () => { await assert.rejects( () => generate({ seed: '🚀'.repeat(9) }), @@ -144,4 +170,52 @@ describe('DeployToken (cct/solana)', () => { ) }) }) + + describe('execute', () => { + it('signs, submits, and returns the mint address', async () => { + const result = await new DeployToken().execute(submitChain(), { + wallet: WALLET, + decimals: 9, + withMetaplex: false, + }) + + assert.equal(result.hash, HASH) + assert.match(result.tokenAddress, /^[1-9A-HJ-NP-Za-km-z]+$/) + }) + + it('returns the metadata address when creating Metaplex metadata', async () => { + const result = await new DeployToken().execute(submitChain(), { + wallet: WALLET, + decimals: 9, + withMetaplex: true, + name: 'My Token', + symbol: 'MTK', + }) + + assert.equal(result.hash, HASH) + assert.match(result.metadataAddress!, /^[1-9A-HJ-NP-Za-km-z]+$/) + }) + + 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( + () => + new DeployToken().execute(stubChain(), { + 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', + ) + }) + }) }) diff --git a/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts index 64501626d..ff8b8455a 100644 --- a/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts @@ -19,6 +19,7 @@ import { type SolanaGenerateParams, SolanaOperation, } from '../../operation.ts' +import { deriveMetadataAddress } from '../../programs/token.ts' import { submit } from '../../submit.ts' import { validateOptionalPublicKey, validatePublicKey } from '../../validate.ts' @@ -76,19 +77,10 @@ export type ExecuteDeployTokenResult = TransactionResult & { 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'), @@ -319,7 +311,7 @@ export class DeployToken extends SolanaOperation< const lamports = await chain.connection.getMinimumBalanceForRentExemption(getMintLen([])) const instructions = createMintInstructions(mint, lamports, params.decimals, config) - const metadataAddress = params.withMetaplex ? deriveMetadataAddress(mint) : undefined + const metadataAddress = params.withMetaplex ? deriveMetadataAddress(mint).toBase58() : undefined if (params.withMetaplex) instructions.push( ...(await createMetadataInstructions( diff --git a/ccip-sdk/src/cct/solana/token/operations/index.ts b/ccip-sdk/src/cct/solana/token/operations/index.ts index 037158331..ac2fc6043 100644 --- a/ccip-sdk/src/cct/solana/token/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token/operations/index.ts @@ -3,3 +3,4 @@ export * from './create-token-account.ts' export * from './deploy-token.ts' export * from './mint-tokens.ts' export * from './set-token-authority.ts' +export * from './update-metadata-authority.ts' diff --git a/ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts index 77950c93d..26b4d420f 100644 --- a/ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts +++ b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts @@ -16,8 +16,8 @@ import { import { ChainFamily } from '../../../../networks.ts' import type { SolanaChain } from '../../../../solana/index.ts' import { CCTParamsInvalidError } from '../../../errors.ts' -import { SolanaTokenManager } from '../../index.ts' import { U64_MAX } from '../../validate.ts' +import { MintTokens } from './mint-tokens.ts' const TOKEN = Keypair.generate().publicKey const PAYER = Keypair.generate().publicKey.toBase58() @@ -58,7 +58,7 @@ function submitChain(): SolanaChain { } function generate(opts: Record = {}, mintOwner?: PublicKey | null) { - return SolanaTokenManager.fromChain(chain(mintOwner)).generateUnsignedMintTokens({ + return new MintTokens().generate(chain(mintOwner), { payer: PAYER, tokenAddress: TOKEN.toBase58(), recipient: RECIPIENT.toBase58(), @@ -120,7 +120,7 @@ describe('MintTokens (cct/solana)', () => { await assert.rejects( () => - SolanaTokenManager.fromChain(missingAtaChain).generateUnsignedMintTokens({ + new MintTokens().generate(missingAtaChain, { payer: PAYER, tokenAddress: TOKEN.toBase58(), recipient: RECIPIENT.toBase58(), @@ -177,7 +177,7 @@ describe('MintTokens (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).mintTokens({ + const result = await new MintTokens().execute(submitChain(), { tokenAddress: TOKEN.toBase58(), recipient: RECIPIENT.toBase58(), amount: 1n, @@ -189,7 +189,7 @@ describe('MintTokens (cct/solana)', () => { it('requires unsigned generation for SPL multisig authorities', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).mintTokens({ + new MintTokens().execute(chain(), { tokenAddress: TOKEN.toBase58(), recipient: RECIPIENT.toBase58(), amount: 1n, @@ -205,7 +205,7 @@ describe('MintTokens (cct/solana)', () => { it('rejects a non-wallet authority for signed minting', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).mintTokens({ + new MintTokens().execute(chain(), { tokenAddress: TOKEN.toBase58(), recipient: RECIPIENT.toBase58(), amount: 1n, diff --git a/ccip-sdk/src/cct/solana/token/operations/set-token-authority.test.ts b/ccip-sdk/src/cct/solana/token/operations/set-token-authority.test.ts index 186dd7164..850a29fe6 100644 --- a/ccip-sdk/src/cct/solana/token/operations/set-token-authority.test.ts +++ b/ccip-sdk/src/cct/solana/token/operations/set-token-authority.test.ts @@ -8,7 +8,7 @@ import { CCIPTokenMintInvalidError, CCIPTokenMintNotFoundError } from '../../../ import { ChainFamily } from '../../../../networks.ts' import type { SolanaChain } from '../../../../solana/index.ts' import { CCTParamsInvalidError } from '../../../errors.ts' -import { SolanaTokenManager } from '../../index.ts' +import { SetTokenAuthority } from './set-token-authority.ts' const TOKEN = Keypair.generate().publicKey.toBase58() const PAYER = Keypair.generate().publicKey.toBase58() @@ -48,7 +48,7 @@ function submitChain(): SolanaChain { } function generate(opts: Record = {}, mintOwner?: PublicKey | null) { - return SolanaTokenManager.fromChain(chain(mintOwner)).generateUnsignedSetTokenAuthority({ + return new SetTokenAuthority().generate(chain(mintOwner), { tokenAddress: TOKEN, payer: PAYER, authority: AUTHORITY, @@ -195,7 +195,7 @@ describe('SetTokenAuthority (cct/solana)', () => { describe('execute', () => { it('signs, submits, and returns the tx hash', async () => { - const result = await SolanaTokenManager.fromChain(submitChain()).setTokenAuthority({ + const result = await new SetTokenAuthority().execute(submitChain(), { tokenAddress: TOKEN, newAuthority: NEW_AUTHORITY, authorityTypes: ['mint'], @@ -208,7 +208,7 @@ describe('SetTokenAuthority (cct/solana)', () => { it('requires unsigned generation for SPL multisig authorities', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).setTokenAuthority({ + new SetTokenAuthority().execute(chain(), { tokenAddress: TOKEN, newAuthority: NEW_AUTHORITY, authority: MULTISIG, @@ -224,7 +224,7 @@ describe('SetTokenAuthority (cct/solana)', () => { it('rejects a non-wallet authority for signed updates', async () => { await assert.rejects( () => - SolanaTokenManager.fromChain(chain()).setTokenAuthority({ + new SetTokenAuthority().execute(chain(), { tokenAddress: TOKEN, newAuthority: NEW_AUTHORITY, authority: AUTHORITY, diff --git a/ccip-sdk/src/cct/solana/token/operations/update-metadata-authority.test.ts b/ccip-sdk/src/cct/solana/token/operations/update-metadata-authority.test.ts new file mode 100644 index 000000000..4502ea3ae --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/update-metadata-authority.test.ts @@ -0,0 +1,174 @@ +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, CCTTxFailedError } from '../../../errors.ts' +import { UpdateMetadataAuthority } from './update-metadata-authority.ts' + +const METAPLEX_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s') +const TOKEN = Keypair.generate().publicKey.toBase58() +const PAYER = Keypair.generate().publicKey.toBase58() +const AUTHORITY = Keypair.generate().publicKey.toBase58() +const NEW_AUTHORITY = Keypair.generate().publicKey.toBase58() +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: new PublicKey(AUTHORITY), + signTransaction: async (tx: T) => tx, +} + +function metadataData(authority = AUTHORITY, isMutable = true, mint = TOKEN): Buffer { + return Buffer.concat([ + Buffer.from([4]), // MetadataV1 + new PublicKey(authority).toBuffer(), + new PublicKey(mint).toBuffer(), + Buffer.alloc(12), // Empty name, symbol, and URI strings. + Buffer.alloc(2), // Seller fee basis points. + Buffer.from([0, 0, isMutable ? 1 : 0, 0, 0, 0, 0, 0]), + ]) +} + +function metadataAccount(metadata = metadataData()) { + return { + data: metadata, + executable: false, + lamports: 0, + owner: METAPLEX_PROGRAM_ID, + rentEpoch: 0, + } +} + +function chain(metadata: Buffer | null = metadataData()): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + rpcEndpoint: 'http://localhost:8899', + getAccountInfo: async () => (metadata ? metadataAccount(metadata) : null), + }, + } as unknown as SolanaChain +} + +function submitChain(): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + rpcEndpoint: 'http://localhost:8899', + getAccountInfo: async () => metadataAccount(), + 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: Record = {}, metadata?: Buffer | null) { + return new UpdateMetadataAuthority().generate(chain(metadata), { + tokenAddress: TOKEN, + payer: PAYER, + authority: AUTHORITY, + newAuthority: NEW_AUTHORITY, + ...opts, + }) +} + +describe('UpdateMetadataAuthority (cct/solana)', () => { + describe('generate', () => { + it('builds a Metaplex UpdateV1 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(unsigned.instructions.length, 1) + assert.equal(instruction.programId.toBase58(), METAPLEX_PROGRAM_ID.toBase58()) + assert.equal(instruction.data[0], 50) // Update + assert.equal(instruction.data[1], 0) // UpdateV1 + assert.equal(instruction.keys[0]!.pubkey.toBase58(), AUTHORITY) + assert.equal(instruction.keys[0]!.isSigner, true) + }) + + it('defaults authority to payer', async () => { + const unsigned = await generate({ authority: undefined }, metadataData(PAYER)) + + assert.equal(unsigned.instructions[0]!.keys[0]!.pubkey.toBase58(), PAYER) + }) + }) + + describe('validation', () => { + it('rejects invalid public keys before RPC', async () => { + for (const param of ['tokenAddress', 'newAuthority', 'authority']) { + await assert.rejects( + () => generate({ [param]: 'invalid' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + + it('requires Metaplex metadata with the current authority', async () => { + for (const [metadata, param] of [ + [null, 'tokenAddress'], + [metadataData(AUTHORITY, true, PAYER), 'tokenAddress'], + [metadataData(PAYER), 'authority'], + [Buffer.alloc(0), 'tokenAddress'], + ] as const) { + await assert.rejects( + () => generate({}, metadata), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + } + }) + + it('reports the supplied and current authority on mismatch', async () => { + await assert.rejects( + () => generate({}, metadataData(PAYER)), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.message.includes(AUTHORITY) && + err.message.includes(PAYER), + ) + }) + + it('rejects immutable metadata before submission', async () => { + await assert.rejects( + () => generate({}, metadataData(AUTHORITY, false)), + (err: unknown) => + err instanceof CCTTxFailedError && err.message.includes('metadata is immutable'), + ) + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + const result = await new UpdateMetadataAuthority().execute(submitChain(), { + tokenAddress: TOKEN, + newAuthority: NEW_AUTHORITY, + wallet: WALLET, + }) + + assert.deepEqual(result, { hash: HASH }) + }) + + it('rejects a non-wallet authority for signed updates', async () => { + await assert.rejects( + () => + new UpdateMetadataAuthority().execute(chain(), { + tokenAddress: TOKEN, + newAuthority: NEW_AUTHORITY, + authority: PAYER, + wallet: WALLET, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'updateMetadataAuthority' && + err.context.param === 'authority', + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/update-metadata-authority.ts b/ccip-sdk/src/cct/solana/token/operations/update-metadata-authority.ts new file mode 100644 index 000000000..2d713ae18 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/update-metadata-authority.ts @@ -0,0 +1,190 @@ +import type { MetadataAccountData } from '@metaplex-foundation/mpl-token-metadata' +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 { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts' +import type { TransactionResult } from '../../../operation.ts' +import { + type SolanaExecuteParams, + type SolanaGenerateParams, + SolanaOperation, +} from '../../operation.ts' +import { deriveMetadataAddress } from '../../programs/token.ts' +import { submit } from '../../submit.ts' +import { parsePublicKey, validateAuthorityMatchesWallet } from '../../validate.ts' +import { METADATA_PROGRAM_ID } from '../constants.ts' + +type UpdateMetadataAuthorityParams = { + /** SPL token mint address with Metaplex metadata. */ + tokenAddress: string + /** Address to receive the Metaplex metadata update authority. */ + newAuthority: string + /** Current metadata update authority. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +type ParsedUpdateMetadataAuthorityParams = { + tokenAddress: PublicKey + newAuthority: PublicKey + authority: PublicKey + payer: PublicKey +} + +function validateMetadataAuthority( + operation: string, + metadata: MetadataAccountData, + { authority }: ParsedUpdateMetadataAuthorityParams, +): void { + if (!new PublicKey(metadata.updateAuthority).equals(authority)) { + throw new CCTParamsInvalidError( + operation, + 'authority', + `${authority.toBase58()} is not the current metadata update authority (${metadata.updateAuthority})`, + ) + } + if (!metadata.isMutable) { + throw new CCTTxFailedError(operation, 'metadata is immutable and cannot be updated') + } +} + +/** Parameters for unsigned Solana Metaplex metadata authority update. */ +export type GenerateUpdateMetadataAuthorityParams = + SolanaGenerateParams + +/** Unsigned Solana Metaplex metadata authority update result. */ +export type GenerateUpdateMetadataAuthorityResult = UnsignedSolanaTx + +/** Parameters for executing Solana Metaplex metadata authority update. */ +export type ExecuteUpdateMetadataAuthorityParams = + SolanaExecuteParams + +/** Result of executing Solana Metaplex metadata authority update. */ +export type ExecuteUpdateMetadataAuthorityResult = TransactionResult + +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 { + createNoopSigner: umi.createNoopSigner, + createUmi: bundleDefaults.createUmi, + getMetadataAccountDataSerializer: metadata.getMetadataAccountDataSerializer, + mplTokenMetadata: metadata.mplTokenMetadata, + publicKey: umi.publicKey, + signerIdentity: umi.signerIdentity, + toWeb3JsInstruction: web3.toWeb3JsInstruction, + updateV1: metadata.updateV1, + } +} + +async function getMetadata( + operation: string, + chain: SolanaChain, + tokenAddress: PublicKey, + metaplex: Awaited>, +): Promise { + const metadata = await chain.connection.getAccountInfo(deriveMetadataAddress(tokenAddress)) + if (!metadata || !metadata.owner.equals(METADATA_PROGRAM_ID)) { + throw new CCTParamsInvalidError( + operation, + 'tokenAddress', + 'mint not found or does not have Metaplex metadata', + ) + } + + try { + return metaplex.getMetadataAccountDataSerializer().deserialize(metadata.data)[0] + } catch { + throw new CCTParamsInvalidError( + operation, + 'tokenAddress', + 'mint not found or does not have Metaplex metadata', + ) + } +} + +/** Transfers the Metaplex metadata update authority for an SPL token mint. */ +export class UpdateMetadataAuthority extends SolanaOperation< + UpdateMetadataAuthorityParams, + UnsignedSolanaTx, + ParsedUpdateMetadataAuthorityParams +> { + readonly name = 'updateMetadataAuthority' + + /** Parses the mint and current and new metadata update authorities. */ + protected override parse( + params: GenerateUpdateMetadataAuthorityParams, + ): ParsedUpdateMetadataAuthorityParams { + const payer = parsePublicKey(this.name, 'payer', params.payer) + return { + tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + newAuthority: parsePublicKey(this.name, 'newAuthority', params.newAuthority), + authority: + params.authority === undefined + ? payer + : parsePublicKey(this.name, 'authority', params.authority), + payer, + } + } + + /** Validates the Metaplex metadata account and builds its `UpdateV1` instruction. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedUpdateMetadataAuthorityParams, + ): Promise { + const metaplex = await loadMetaplex() + const authority = metaplex.createNoopSigner(metaplex.publicKey(opts.authority.toBase58())) + const umi = metaplex + .createUmi(chain.connection) + .use(metaplex.mplTokenMetadata()) + .use( + metaplex.signerIdentity( + metaplex.createNoopSigner(metaplex.publicKey(opts.payer.toBase58())), + ), + ) + + const metadata = await getMetadata(this.name, chain, opts.tokenAddress, metaplex) + validateMetadataAuthority(this.name, metadata, opts) + + const mint = metaplex.publicKey(opts.tokenAddress.toBase58()) + + const instructions: TransactionInstruction[] = metaplex + .updateV1(umi, { + mint, + authority, + newUpdateAuthority: metaplex.publicKey(opts.newAuthority.toBase58()), + }) + .getInstructions() + .map(metaplex.toWeb3JsInstruction) + + chain.logger.debug( + `${this.name}: token = ${opts.tokenAddress.toBase58()}, newAuthority = ${opts.newAuthority.toBase58()}`, + ) + return { family: ChainFamily.Solana, instructions, mainIndex: 0 } + } + + /** Generate, sign, simulate, send, and confirm with the current metadata authority wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteUpdateMetadataAuthorityParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'updateMetadataAuthority requires authority to be the executing wallet. Use generateUnsignedUpdateMetadataAuthority for externally signed transactions.', + ) + } + + return submit(chain, wallet, await this.buildUnsigned(chain, parsed), this.name, computeUnits) + } +} diff --git a/ccip-sdk/src/cct/solana/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts index 769753352..161a3d587 100644 --- a/ccip-sdk/src/cct/solana/validate.test.ts +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -1,16 +1,22 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' +import { MINT_SIZE, MintLayout, TOKEN_PROGRAM_ID } from '@solana/spl-token' import { PublicKey } from '@solana/web3.js' -import { CCTParamsInvalidError } from '../errors.ts' +import { CCIPTokenAccountNotFoundError } from '../../errors/index.ts' +import { CCTParamsInvalidError, CCTTxFailedError } from '../errors.ts' import { type PoolProgramRef, TOKEN_POOL_PROGRAMS } from './programs/token-pool.ts' import { parseHexBytes, parseNonEmptyHexBytes, parsePublicKey, + resolveExistingTokenAccount, + resolveLockReleasePoolProgram, resolvePoolProgram, + validateAuthorityMatchesWallet, validateBigInt, + validateDelegation, validateInteger, validateNonEmptyString, validateOptionalPublicKey, @@ -20,6 +26,23 @@ import { validateWritableIndexes, } from './validate.ts' +function mintData() { + const data = Buffer.alloc(MINT_SIZE) + MintLayout.encode( + { + mintAuthorityOption: 1, + mintAuthority: PublicKey.default, + supply: 0n, + decimals: 6, + isInitialized: true, + freezeAuthorityOption: 0, + freezeAuthority: PublicKey.default, + }, + data, + ) + return data +} + describe('Validate (cct/solana)', () => { it('parses valid public keys', () => { const key = parsePublicKey('op', 'payer', PublicKey.default.toBase58()) @@ -108,6 +131,21 @@ describe('Validate (cct/solana)', () => { ) }) + it('validates the executing authority', () => { + const authority = PublicKey.default + + assert.doesNotThrow(() => validateAuthorityMatchesWallet('op', authority, authority)) + assert.throws( + () => + validateAuthorityMatchesWallet( + 'op', + authority, + new PublicKey(Uint8Array.from({ length: 32 }, () => 1)), + ), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'authority', + ) + }) + it('validates pool types', () => { assert.doesNotThrow(() => validatePoolType('op', 'poolType', 'burn-mint')) assert.doesNotThrow(() => validatePoolType('op', 'poolType', 'lock-release')) @@ -156,6 +194,14 @@ describe('Validate (cct/solana)', () => { ) }) + it('resolves lock-release pool programs only', () => { + assert.ok(resolveLockReleasePoolProgram('op', { poolType: 'lock-release' })) + assert.throws( + () => resolveLockReleasePoolProgram('op', { poolType: 'burn-mint' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'poolType', + ) + }) + it('validates integers', () => { assert.doesNotThrow(() => validateInteger('op', 'threshold', 1)) assert.doesNotThrow(() => validateInteger('op', 'decimals', 255, 0, 255)) @@ -163,6 +209,9 @@ describe('Validate (cct/solana)', () => { () => validateInteger('op', 'decimals', 256, 0, 255), (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'decimals', ) + assert.throws(() => validateInteger('op', 'threshold', 0, 1), CCTParamsInvalidError) + assert.throws(() => validateInteger('op', 'limit', 2, undefined, 1), CCTParamsInvalidError) + assert.throws(() => validateInteger('op', 'integer', 1.5), CCTParamsInvalidError) }) it('validates bigint bounds with useful errors', () => { @@ -179,6 +228,57 @@ describe('Validate (cct/solana)', () => { ) }) + it('validates token delegation', () => { + const tokenAccount = PublicKey.default + const delegate = new PublicKey(Uint8Array.from({ length: 32 }, () => 1)) + const otherDelegate = new PublicKey(Uint8Array.from({ length: 32 }, () => 2)) + + assert.doesNotThrow(() => + validateDelegation( + 'op', + tokenAccount, + { delegate, delegatedAmount: 2n } as never, + delegate, + 2n, + ), + ) + for (const account of [ + { delegate: null, delegatedAmount: 2n }, + { delegate: otherDelegate, delegatedAmount: 2n }, + { delegate, delegatedAmount: 1n }, + ]) { + assert.throws( + () => validateDelegation('op', tokenAccount, account as never, delegate, 2n), + (err: unknown) => err instanceof CCTTxFailedError, + ) + } + }) + + it('maps missing token accounts and preserves other lookup errors', async () => { + const mint = new PublicKey(Uint8Array.from({ length: 32 }, () => 1)) + const holder = new PublicKey(Uint8Array.from({ length: 32 }, () => 2)) + const tokenAccount = new PublicKey(Uint8Array.from({ length: 32 }, () => 3)) + const connection = { + getAccountInfo: async (address: PublicKey) => + address.equals(mint) ? { owner: TOKEN_PROGRAM_ID, data: mintData() } : null, + } + + await assert.rejects( + () => resolveExistingTokenAccount(connection as never, mint, holder, tokenAccount), + (err: unknown) => err instanceof CCIPTokenAccountNotFoundError, + ) + + const invalidConnection = { + getAccountInfo: async (address: PublicKey) => + address.equals(mint) + ? { owner: TOKEN_PROGRAM_ID, data: mintData() } + : { owner: TOKEN_PROGRAM_ID, data: Buffer.alloc(0) }, + } + await assert.rejects(() => + resolveExistingTokenAccount(invalidConnection as never, mint, holder, tokenAccount), + ) + }) + it('accepts omitted and valid writable indexes', () => { assert.doesNotThrow(() => validateWritableIndexes('op', 'writableIndexes', undefined)) assert.doesNotThrow(() => validateWritableIndexes('op', 'writableIndexes', [0, 3, 255])) From 98db9fecd441638fc45ec2f572ad2126ade591ae Mon Sep 17 00:00:00 2001 From: Mervin Date: Mon, 7 Sep 2026 17:16:43 +0800 Subject: [PATCH 81/87] feat(cct-sdk): Add owner override pending admin op solana (#399) * feat: add transfer pool ownership op solana * feat: add accept pool ownership op solana * feat: add transfer authority op solana * fix: update export barrel * feat: add mint tokens op solana * fix: address comments * fix: address comments * feat: add set can accept liquidity op solana * fix: add lock release token pool idl * feat: add set rebalancer op solana * fix: update tsdoc * feat: add approve token op solana * feat: add provider liquidity op solana * fix: address comments * fix: revert unrelated changes * fix: add preflight checks * fix: update tsdoc * fix: extract validate pool liquidity config * feat: add withdraw liquidity op solana * fix: add preflight checks * feat: add update metadata authority op solana * fix: lint errors * fix: lint errors * fix: address comments * fix: refactor unit tests * fix: refactor export * feat: add owner override pending admint op solana * fix: update tsdoc and add preflight check * fix: update tsdoc @example * fix: update tsdoc @throws * feat(cct-sdk): Add includeApproval option to provide liquidity (#401) feat: add includeApproval option in provide liquidity --- ccip-sdk/src/cct/solana/index.test.ts | 179 +++++++++++++++--- ccip-sdk/src/cct/solana/index.ts | 111 ++++++++--- .../token-admin-registry/operations/index.ts | 1 + ...ner-override-pending-administrator.test.ts | 169 +++++++++++++++++ .../owner-override-pending-administrator.ts | 136 +++++++++++++ .../operations/provide-liquidity.test.ts | 34 ++-- .../operations/provide-liquidity.ts | 44 +++-- 7 files changed, 602 insertions(+), 72 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/owner-override-pending-administrator.test.ts create mode 100644 ccip-sdk/src/cct/solana/token-admin-registry/operations/owner-override-pending-administrator.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 18d96c10c..9f0551a7c 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -87,6 +87,8 @@ describe('SolanaTokenManager (cct/solana)', () => { const pool = Keypair.generate().publicKey.toBase58() const account = Keypair.generate().publicKey.toBase58() const reader = Keypair.generate().publicKey.toBase58() + const overrideAddress = Keypair.generate().publicKey.toBase58() + const overrideRouter = Keypair.generate().publicKey.toBase58() const remoteChainSelector = 5009297550715157269n function chain(): SolanaChain { @@ -182,7 +184,9 @@ describe('SolanaTokenManager (cct/solana)', () => { }, }, }), - simulateTransaction: async () => ({ value: { err: null, logs: [], unitsConsumed: 1 } }), + simulateTransaction: async () => ({ + value: { err: null, logs: [], unitsConsumed: 1 }, + }), getLatestBlockhash: async () => ({ blockhash: PublicKey.default.toBase58(), lastValidBlockHeight: 1, @@ -190,10 +194,17 @@ describe('SolanaTokenManager (cct/solana)', () => { sendTransaction: async () => PublicKey.default.toBase58(), confirmTransaction: async () => ({ value: { err: null } }), }, - getTokenAdminRegistryFor: async (address: string) => (address === reader ? pool : account), + getTokenAdminRegistryFor: async (address: string) => + address === reader ? pool : address === overrideAddress ? overrideRouter : account, getSupportedTokens: async () => [mint], getTokenPoolRemotes: async () => ({}), - getRegistryTokenConfig: async () => ({ administrator: payer, pendingAdministrator: payer }), + getRegistryTokenConfig: async (router: string) => + router === overrideRouter + ? { + administrator: PublicKey.default.toBase58(), + pendingAdministrator: payer, + } + : { administrator: payer, pendingAdministrator: payer }, } as unknown as SolanaChain } @@ -212,11 +223,21 @@ describe('SolanaTokenManager (cct/solana)', () => { > = [ [ 'deployToken', - () => cct.generateUnsignedDeployToken({ payer, decimals: 6, withMetaplex: false }), + () => + cct.generateUnsignedDeployToken({ + payer, + decimals: 6, + withMetaplex: false, + }), ], [ 'approveToken', - () => cct.generateUnsignedApproveToken({ ...common, delegate: account, amount: 1n }), + () => + cct.generateUnsignedApproveToken({ + ...common, + delegate: account, + amount: 1n, + }), ], [ 'createTokenAccount', @@ -229,7 +250,12 @@ describe('SolanaTokenManager (cct/solana)', () => { ], [ 'mintTokens', - () => cct.generateUnsignedMintTokens({ ...common, recipient: account, amount: 1n }), + () => + cct.generateUnsignedMintTokens({ + ...common, + recipient: account, + amount: 1n, + }), ], [ 'setTokenAuthority', @@ -242,7 +268,11 @@ describe('SolanaTokenManager (cct/solana)', () => { ], [ 'updateMetadataAuthority', - () => cct.generateUnsignedUpdateMetadataAuthority({ ...common, newAuthority: account }), + () => + cct.generateUnsignedUpdateMetadataAuthority({ + ...common, + newAuthority: account, + }), ], [ 'createTokenMultisig', @@ -257,12 +287,20 @@ describe('SolanaTokenManager (cct/solana)', () => { [ 'createLookupTable', () => - cct.generateUnsignedCreateLookupTable({ payer, authority: payer, mode: 'createEmpty' }), + cct.generateUnsignedCreateLookupTable({ + payer, + authority: payer, + mode: 'createEmpty', + }), ], [ 'configureAllowlist', () => - cct.generateUnsignedConfigureAllowlist({ ...common, add: [account], enabled: true }), + cct.generateUnsignedConfigureAllowlist({ + ...common, + add: [account], + enabled: true, + }), ], ['deployTokenPool', () => cct.generateUnsignedDeployTokenPool(common)], [ @@ -304,11 +342,19 @@ describe('SolanaTokenManager (cct/solana)', () => { ], [ 'deleteChainRemoteConfig', - () => cct.generateUnsignedDeleteChainRemoteConfig({ ...common, remoteChainSelector }), + () => + cct.generateUnsignedDeleteChainRemoteConfig({ + ...common, + remoteChainSelector, + }), ], [ 'setRateLimitAdmin', - () => cct.generateUnsignedSetRateLimitAdmin({ ...common, newRateLimitAdmin: account }), + () => + cct.generateUnsignedSetRateLimitAdmin({ + ...common, + newRateLimitAdmin: account, + }), ], ['provideLiquidity', () => cct.generateUnsignedProvideLiquidity({ ...common, amount: 1n })], [ @@ -317,15 +363,27 @@ describe('SolanaTokenManager (cct/solana)', () => { ], [ 'setCanAcceptLiquidity', - () => cct.generateUnsignedSetCanAcceptLiquidity({ ...common, allow: true }), + () => + cct.generateUnsignedSetCanAcceptLiquidity({ + ...common, + allow: true, + }), ], [ 'setRebalancer', - () => cct.generateUnsignedSetRebalancer({ ...common, rebalancer: account }), + () => + cct.generateUnsignedSetRebalancer({ + ...common, + rebalancer: account, + }), ], [ 'transferOwnership', - () => cct.generateUnsignedTransferOwnership({ ...common, newOwner: account }), + () => + cct.generateUnsignedTransferOwnership({ + ...common, + newOwner: account, + }), ], ['acceptOwnership', () => cct.generateUnsignedAcceptOwnership(common)], [ @@ -359,10 +417,23 @@ describe('SolanaTokenManager (cct/solana)', () => { }), ], ['acceptAdmin', () => cct.generateUnsignedAcceptAdmin({ ...common, address: account })], + [ + 'ownerOverridePendingAdministrator', + () => + cct.generateUnsignedOwnerOverridePendingAdministrator({ + ...common, + address: overrideAddress, + newAdmin: account, + }), + ], ['registerAdmin', () => cct.generateUnsignedRegisterAdmin({ ...common, address: account })], [ 'removeFromAllowlist', - () => cct.generateUnsignedRemoveFromAllowlist({ ...common, remove: [account] }), + () => + cct.generateUnsignedRemoveFromAllowlist({ + ...common, + remove: [account], + }), ], [ 'setPool', @@ -376,7 +447,11 @@ describe('SolanaTokenManager (cct/solana)', () => { [ 'transferAdmin', () => - cct.generateUnsignedTransferAdmin({ ...common, address: account, newAdmin: account }), + cct.generateUnsignedTransferAdmin({ + ...common, + address: account, + newAdmin: account, + }), ], ] @@ -391,22 +466,42 @@ describe('SolanaTokenManager (cct/solana)', () => { it('runs every signed facade operation', async () => { const cct = SolanaTokenManager.fromChain(facadeChain) - const wallet = { publicKey: new PublicKey(payer), signTransaction: async (tx: T) => tx } + const wallet = { + publicKey: new PublicKey(payer), + signTransaction: async (tx: T) => tx, + } const signed: Array< [string, () => Promise<{ hash: string } | { hash: string }[] | { hashes: string[] }>] > = [ ['deployToken', () => cct.deployToken({ wallet, decimals: 6, withMetaplex: false })], [ 'approveToken', - () => cct.approveToken({ wallet, tokenAddress: mint, delegate: account, amount: 1n }), + () => + cct.approveToken({ + wallet, + tokenAddress: mint, + delegate: account, + amount: 1n, + }), ], [ 'createTokenAccount', - () => cct.createTokenAccount({ wallet, tokenAddress: mint, ownerAddress: account }), + () => + cct.createTokenAccount({ + wallet, + tokenAddress: mint, + ownerAddress: account, + }), ], [ 'mintTokens', - () => cct.mintTokens({ wallet, tokenAddress: mint, recipient: account, amount: 1n }), + () => + cct.mintTokens({ + wallet, + tokenAddress: mint, + recipient: account, + amount: 1n, + }), ], [ 'setTokenAuthority', @@ -420,7 +515,12 @@ describe('SolanaTokenManager (cct/solana)', () => { ], [ 'updateMetadataAuthority', - () => cct.updateMetadataAuthority({ wallet, tokenAddress: mint, newAuthority: account }), + () => + cct.updateMetadataAuthority({ + wallet, + tokenAddress: mint, + newAuthority: account, + }), ], [ 'createTokenMultisig', @@ -446,7 +546,12 @@ describe('SolanaTokenManager (cct/solana)', () => { ], [ 'deployTokenPool', - () => cct.deployTokenPool({ wallet, tokenAddress: mint, poolType: 'lock-release' }), + () => + cct.deployTokenPool({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + }), ], [ 'applyChainUpdates', @@ -563,7 +668,12 @@ describe('SolanaTokenManager (cct/solana)', () => { ], [ 'acceptOwnership', - () => cct.acceptOwnership({ wallet, tokenAddress: mint, poolType: 'lock-release' }), + () => + cct.acceptOwnership({ + wallet, + tokenAddress: mint, + poolType: 'lock-release', + }), ], [ 'setChainRateLimit', @@ -600,6 +710,16 @@ describe('SolanaTokenManager (cct/solana)', () => { }), ], ['acceptAdmin', () => cct.acceptAdmin({ wallet, tokenAddress: mint, address: account })], + [ + 'ownerOverridePendingAdministrator', + () => + cct.ownerOverridePendingAdministrator({ + wallet, + tokenAddress: mint, + address: overrideAddress, + newAdmin: account, + }), + ], [ 'registerAdmin', () => cct.registerAdmin({ wallet, tokenAddress: mint, address: account }), @@ -627,7 +747,12 @@ describe('SolanaTokenManager (cct/solana)', () => { [ 'transferAdmin', () => - cct.transferAdmin({ wallet, tokenAddress: mint, address: account, newAdmin: account }), + cct.transferAdmin({ + wallet, + tokenAddress: mint, + address: account, + newAdmin: account, + }), ], ] @@ -656,7 +781,11 @@ describe('SolanaTokenManager (cct/solana)', () => { ], [ 'getTokenPoolState', - () => cct.getTokenPoolState({ tokenAddress: mint, poolType: 'lock-release' }), + () => + cct.getTokenPoolState({ + tokenAddress: mint, + poolType: 'lock-release', + }), ], [ 'getTokenAdminRegistry', diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index c9dc5a4a0..c7694a525 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -19,6 +19,8 @@ import { type ExecuteAppendToLookupTableResult, type ExecuteCreateLookupTableParams, type ExecuteCreateLookupTableResult, + type ExecuteOwnerOverridePendingAdministratorParams, + type ExecuteOwnerOverridePendingAdministratorResult, type ExecuteRegisterAdminParams, type ExecuteRegisterAdminResult, type ExecuteSetPoolParams, @@ -31,6 +33,8 @@ import { type GenerateAppendToLookupTableResult, type GenerateCreateLookupTableParams, type GenerateCreateLookupTableResult, + type GenerateOwnerOverridePendingAdministratorParams, + type GenerateOwnerOverridePendingAdministratorResult, type GenerateRegisterAdminParams, type GenerateRegisterAdminResult, type GenerateSetPoolParams, @@ -45,6 +49,7 @@ import { CreateLookupTable, GetSupportedTokens, GetTokenAdminRegistry, + OwnerOverridePendingAdministrator, RegisterAdmin, SetPool, TransferAdmin, @@ -195,6 +200,7 @@ export class SolanaTokenManager extends TokenManager readonly #createLookupTable = new CreateLookupTable() readonly #getSupportedTokens = new GetSupportedTokens() readonly #getTokenAdminRegistry = new GetTokenAdminRegistry() + readonly #ownerOverridePendingAdministrator = new OwnerOverridePendingAdministrator() readonly #registerAdmin = new RegisterAdmin() readonly #setPool = new SetPool() readonly #transferAdmin = new TransferAdmin() @@ -1235,7 +1241,8 @@ export class SolanaTokenManager extends TokenManager * * @remarks The pool config must have `canAcceptLiquidity: true` and a `rebalancer` equal to the * transaction authority. The authority's ATA for `tokenAddress` must exist, hold at least `amount`, - * and delegate at least `amount` to the pool signer PDA; use {@link generateUnsignedApproveToken}. + * and delegate at least `amount` to the pool signer PDA. Set `includeApproval: true` to bundle + * that approval before the liquidity instruction in this transaction. * * @see {@link provideLiquidity} * @see {@link generateUnsignedApproveToken} @@ -1248,25 +1255,15 @@ export class SolanaTokenManager extends TokenManager * @throws {@link CCIPTokenPoolStateNotFoundError} If the token pool state is missing. * @throws {@link CCIPTokenAccountNotFoundError} If the rebalancer or pool vault ATA is missing; create it first. * - * @example Prepare and generate liquidity instructions + * @example Generate bundled approval and liquidity instructions * ```ts * const cct = SolanaTokenManager.fromChain(chain) - * const amount = 1_000_000n - * const { config } = await cct.getTokenPoolState({ - * tokenAddress: mint, - * poolType: 'lock-release', - * }) - * const approval = await cct.generateUnsignedApproveToken({ - * payer: rebalancer, - * tokenAddress: mint, - * delegate: config.poolSigner, - * amount, - * }) * const liquidity = await cct.generateUnsignedProvideLiquidity({ * payer: rebalancer, * tokenAddress: mint, * poolType: 'lock-release', - * amount, + * amount: 1_000_000n, + * includeApproval: true, * }) * ``` */ @@ -1284,7 +1281,7 @@ export class SolanaTokenManager extends TokenManager * * @remarks The pool config must have `canAcceptLiquidity: true` and a `rebalancer` equal to the * transaction authority. Before this operation, the rebalancer ATA must delegate at least `amount` - * to the pool signer PDA; use {@link approveToken} first. + * to the pool signer PDA, unless `includeApproval: true` bundles that approval in this transaction. * * @see {@link generateUnsignedProvideLiquidity} * @see {@link approveToken} @@ -1299,19 +1296,19 @@ export class SolanaTokenManager extends TokenManager * @throws {@link CCIPTokenPoolStateNotFoundError} If the token pool state is missing. * @throws {@link CCIPTokenAccountNotFoundError} If the rebalancer or pool vault ATA is missing; create it first. * @throws {@link CCTTxFailedError} If the source ATA does not delegate enough tokens to the pool - * signer, the pool rejects the rebalancer, liquidity is disabled, the token account lacks funds, - * or simulation/submission fails. + * signer and `includeApproval` is false, the pool rejects the rebalancer, liquidity is disabled, + * the token account lacks funds, or simulation/submission fails. * - * @example Prepare and provide liquidity + * @example Approve and provide liquidity in one transaction * ```ts * const cct = SolanaTokenManager.fromChain(chain) - * const amount = 1_000_000n - * const { config } = await cct.getTokenPoolState({ + * await cct.provideLiquidity({ + * wallet, * tokenAddress: mint, * poolType: 'lock-release', + * amount: 1_000_000n, + * includeApproval: true, * }) - * await cct.approveToken({ wallet, tokenAddress: mint, delegate: config.poolSigner, amount }) - * await cct.provideLiquidity({ wallet, tokenAddress: mint, poolType: 'lock-release', amount }) * ``` */ provideLiquidity(opts: ExecuteProvideLiquidityParams): Promise { @@ -1910,6 +1907,76 @@ export class SolanaTokenManager extends TokenManager return this.#acceptAdmin.execute(this.chain, opts) } + /** + * Builds an unsigned instruction that replaces an initial pending registry administrator. + * + * @remarks + * Only the mint authority may authorize this recovery path, and only while the registry has no + * accepted administrator. It replaces the initial pending administrator; the replacement must + * still call {@link generateUnsignedAcceptAdmin}. `authority` defaults to `payer`; use this + * unsigned method for Squads/vault signatures. + * + * @see {@link ownerOverridePendingAdministrator} For wallet-based execution. + * @see {@link generateUnsignedAcceptAdmin} The replacement administrator must accept separately. + * + * @throws {@link CCTParamsInvalidError} If an address is invalid or the registry already has an + * accepted 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.generateUnsignedOwnerOverridePendingAdministrator({ + * tokenAddress: mint, + * address: router, + * newAdmin: replacementAdmin, + * payer: mintAuthority, + * }) + * ``` + */ + generateUnsignedOwnerOverridePendingAdministrator( + opts: GenerateOwnerOverridePendingAdministratorParams, + ): Promise { + return this.#ownerOverridePendingAdministrator.generate(this.chain, opts) + } + + /** + * Replaces an initial pending registry administrator using the mint authority wallet. + * + * @remarks + * This recovery path only works while the registry has no accepted administrator. It replaces the + * initial pending administrator; it does not make the replacement an administrator. The replacement + * must call {@link acceptAdmin} separately. `authority` defaults to `wallet`; use + * {@link generateUnsignedOwnerOverridePendingAdministrator} for Squads/vault flows. + * + * @see {@link generateUnsignedOwnerOverridePendingAdministrator} For externally signed transactions. + * @see {@link acceptAdmin} The replacement administrator must accept the role separately. + * + * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. + * @throws {@link CCTParamsInvalidError} If an address is invalid, the registry already has an accepted + * administrator, or `authority` differs from the wallet. + * @throws {@link CCIPContractNotRouterError} If `address` does not resolve to a Router. + * @throws {@link CCIPTokenNotConfiguredError} If the token is not registered. + * @throws {@link CCTTxFailedError} If the Router rejects a non-mint authority or the registry changes. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * await cct.ownerOverridePendingAdministrator({ + * tokenAddress: mint, + * address: router, + * newAdmin: replacementAdmin, + * wallet: mintAuthorityWallet, + * }) + * ``` + */ + ownerOverridePendingAdministrator( + opts: ExecuteOwnerOverridePendingAdministratorParams, + ): Promise { + return this.#ownerOverridePendingAdministrator.execute(this.chain, opts) + } + /** * Builds an unsigned Solana token registration instruction. * 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 index 0437d089f..8a5fcd7cf 100644 --- a/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/index.ts @@ -3,6 +3,7 @@ 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 './owner-override-pending-administrator.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/owner-override-pending-administrator.test.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/owner-override-pending-administrator.test.ts new file mode 100644 index 000000000..13a910b7e --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/owner-override-pending-administrator.test.ts @@ -0,0 +1,169 @@ +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 { deriveRouterConfigPda, deriveTokenAdminRegistryPda } from '../../programs/router.ts' +import { + type GenerateOwnerOverridePendingAdministratorParams, + OwnerOverridePendingAdministrator, +} from './owner-override-pending-administrator.ts' + +const TOKEN = Keypair.generate().publicKey.toBase58() +const ADDRESS = Keypair.generate().publicKey.toBase58() +const ROUTER = Keypair.generate().publicKey.toBase58() +const OWNER = Keypair.generate().publicKey.toBase58() +const NEW_ADMIN = Keypair.generate().publicKey.toBase58() +const HASH = Keypair.generate().publicKey.toBase58() +const WALLET = { + publicKey: new PublicKey(OWNER), + signTransaction: async (tx: T) => tx, +} + +function stubChain( + administrator = PublicKey.default.toBase58(), + onAddress?: (address: string) => void, +): SolanaChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + 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 } }), + }, + getTokenAdminRegistryFor: async (address: string) => { + onAddress?.(address) + return ROUTER + }, + getRegistryTokenConfig: async () => ({ administrator }), + } as unknown as SolanaChain +} + +function generate(opts: Partial = {}) { + return new OwnerOverridePendingAdministrator().generate(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + newAdmin: NEW_ADMIN, + payer: OWNER, + authority: OWNER, + ...opts, + }) +} + +describe('OwnerOverridePendingAdministrator (cct/solana)', () => { + describe('generate', () => { + it('builds an unsigned owner override pending administrator 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'), 'e66f8695cba876c9') + 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: OWNER, isSigner: true, isWritable: true }, + { pubkey: PublicKey.default.toBase58(), isSigner: false, isWritable: false }, + ], + ) + }) + + it('resolves the router from address', async () => { + let requestedAddress: string | undefined + await new OwnerOverridePendingAdministrator().generate( + stubChain(PublicKey.default.toBase58(), (address) => (requestedAddress = address)), + { tokenAddress: TOKEN, address: ADDRESS, newAdmin: NEW_ADMIN, payer: OWNER }, + ) + + assert.equal(requestedAddress, ADDRESS) + }) + }) + + describe('validation', () => { + for (const param of ['tokenAddress', 'address', 'newAdmin', 'authority'] as const) { + it(`rejects an invalid ${param}`, async () => { + await assert.rejects( + () => generate({ [param]: 'not-a-public-key' }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === param, + ) + }) + } + + it('rejects an accepted registry administrator before building the transaction', async () => { + await assert.rejects( + () => + new OwnerOverridePendingAdministrator().generate(stubChain(OWNER), { + tokenAddress: TOKEN, + address: ADDRESS, + newAdmin: NEW_ADMIN, + payer: OWNER, + }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'tokenAddress' && + typeof err.context.reason === 'string' && + err.context.reason.includes('The current administrator must use transferAdmin instead'), + ) + }) + }) + + describe('execute', () => { + it('signs, submits, and returns the tx hash', async () => { + assert.deepEqual( + await new OwnerOverridePendingAdministrator().execute(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + newAdmin: NEW_ADMIN, + wallet: WALLET, + }), + { hash: HASH }, + ) + }) + + it('requires the mint authority to be the executing wallet', async () => { + await assert.rejects( + () => + new OwnerOverridePendingAdministrator().execute(stubChain(), { + tokenAddress: TOKEN, + address: ADDRESS, + newAdmin: NEW_ADMIN, + authority: NEW_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/owner-override-pending-administrator.ts b/ccip-sdk/src/cct/solana/token-admin-registry/operations/owner-override-pending-administrator.ts new file mode 100644 index 000000000..54f212e3a --- /dev/null +++ b/ccip-sdk/src/cct/solana/token-admin-registry/operations/owner-override-pending-administrator.ts @@ -0,0 +1,136 @@ +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 owner override pending administrator generation and execution. */ +type OwnerOverridePendingAdministratorParams = { + /** Token mint whose pending registry administrator is being replaced. */ + tokenAddress: string + /** CCIP contract to resolve the TokenAdminRegistry/Router from — a Router or OffRamp works. */ + address: string + /** Administrator to propose as the replacement pending administrator. */ + newAdmin: string + /** Mint authority. Defaults to `payer` for single-signer transactions. */ + authority?: string +} + +/** Parameters for unsigned Solana owner override pending administrator generation. */ +export type GenerateOwnerOverridePendingAdministratorParams = + SolanaGenerateParams + +/** Unsigned Solana owner override pending administrator result. */ +export type GenerateOwnerOverridePendingAdministratorResult = UnsignedSolanaTx + +/** Parameters for executing Solana owner override pending administrator. */ +export type ExecuteOwnerOverridePendingAdministratorParams = + SolanaExecuteParams + +/** Result of executing Solana owner override pending administrator. */ +export type ExecuteOwnerOverridePendingAdministratorResult = TransactionResult + +type ParsedOwnerOverridePendingAdministratorParams = { + tokenMint: PublicKey + address: PublicKey + newAdmin: PublicKey + payer: PublicKey + authority: PublicKey +} + +/** Replaces an initial pending TokenAdminRegistry administrator using the mint authority. */ +export class OwnerOverridePendingAdministrator extends SolanaOperation< + OwnerOverridePendingAdministratorParams, + UnsignedSolanaTx, + ParsedOwnerOverridePendingAdministratorParams +> { + readonly name = 'ownerOverridePendingAdministrator' + + /** Parses public keys and defaults authority to payer without mutating caller params. */ + protected override parse( + params: GenerateOwnerOverridePendingAdministratorParams, + ): ParsedOwnerOverridePendingAdministratorParams { + 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 override instruction. The Router verifies the mint authority and initial state on-chain. */ + protected async buildUnsigned( + chain: SolanaChain, + opts: ParsedOwnerOverridePendingAdministratorParams, + ): 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(PublicKey.default)) { + throw new CCTParamsInvalidError( + this.name, + 'tokenAddress', + `cannot override the pending administrator because ${tokenConfig.administrator} has already accepted the role; only initial registrations can be overridden. The current administrator must use transferAdmin instead.`, + ) + } + + const instruction = await createRouterProgram(chain, router, payer) + .methods.ownerOverridePendingAdministrator(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 mint authority wallet. */ + override async execute( + chain: SolanaChain, + params: ExecuteOwnerOverridePendingAdministratorParams, + ): Promise { + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) + + if (params.authority !== undefined) { + validateAuthorityMatchesWallet( + this.name, + parsed.authority, + wallet.publicKey, + 'ownerOverridePendingAdministrator requires authority to be the executing wallet. Use generateUnsignedOwnerOverridePendingAdministrator 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/provide-liquidity.test.ts b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.test.ts index 8fd45334e..c2bba6330 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.test.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.test.ts @@ -14,7 +14,6 @@ import { deriveTokenPoolSignerPda, resolveTokenPoolProgram, } from '../../programs/token-pool.ts' -import { ApproveToken } from '../../token/operations/approve-token.ts' import { ProvideLiquidity } from './provide-liquidity.ts' const TOKEN = Keypair.generate().publicKey.toBase58() @@ -75,6 +74,7 @@ function chain( rebalancer = new PublicKey(AUTHORITY), acceptsLiquidity = true, sourceBalance = 1_000_000n, + delegatedAmount = 1_000_000n, ): SolanaChain { const poolSigner = deriveTokenPoolSignerPda(poolProgram, new PublicKey(TOKEN)) const state = deriveTokenPoolConfigPda(poolProgram, new PublicKey(TOKEN)) @@ -84,7 +84,7 @@ function chain( getAccountInfo: async (address: PublicKey) => address.equals(state) ? { owner: poolProgram, data: poolState(poolProgram, rebalancer, acceptsLiquidity) } - : tokenAccount(poolSigner, 1_000_000n, sourceBalance), + : tokenAccount(poolSigner, delegatedAmount, sourceBalance), }, } as unknown as SolanaChain } @@ -213,23 +213,29 @@ describe('ProvideLiquidity (cct/solana)', () => { assert.equal(unsigned.instructions[0]!.keys[6]!.pubkey.toBase58(), PAYER) }) - it('uses a source account that can be delegated to the pool signer', async () => { + it('bundles approval before liquidity when requested', async () => { const poolProgram = resolveTokenPoolProgram('lock-release') const poolSigner = deriveTokenPoolSignerPda(poolProgram, new PublicKey(TOKEN)) - const approval = await new ApproveToken().generate(chain(), { - payer: AUTHORITY, - tokenAddress: TOKEN, - delegate: poolSigner.toBase58(), - amount: 1_000_000n, - }) - const liquidity = await generate() + const unsigned = await new ProvideLiquidity().generate( + chain(poolProgram, new PublicKey(AUTHORITY), true, 1_000_000n, 0n), + { + payer: PAYER, + tokenAddress: TOKEN, + poolType: 'lock-release', + authority: AUTHORITY, + amount: 1_000_000n, + includeApproval: true, + }, + ) - assert.equal(approval.instructions[0]!.keys[1]!.pubkey.toBase58(), poolSigner.toBase58()) + assert.equal(unsigned.instructions.length, 2) + assert.equal(unsigned.mainIndex, 1) + assert.equal(unsigned.instructions[0]!.keys[1]!.pubkey.toBase58(), poolSigner.toBase58()) + assert.equal(unsigned.instructions[0]!.data.readBigUInt64LE(1), 1_000_000n) assert.equal( - approval.instructions[0]!.keys[0]!.pubkey.toBase58(), - liquidity.instructions[0]!.keys[5]!.pubkey.toBase58(), + lockReleaseTokenPoolCoder.instruction.decode(unsigned.instructions[1]!.data)?.name, + 'provideLiquidity', ) - assert.equal(approval.instructions[0]!.data.readBigUInt64LE(1), 1_000_000n) }) it('supports a compatible custom pool program', async () => { diff --git a/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts index 574a5fbbc..4f8557f3b 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/provide-liquidity.ts @@ -19,6 +19,7 @@ import { deriveTokenPoolSignerPda, } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' +import { ApproveToken } from '../../token/operations/approve-token.ts' import { U64_MAX, parsePublicKey, @@ -37,8 +38,10 @@ type ProvideLiquidityParams = PoolProgramRef & { tokenAddress: string /** Amount to deposit in base units. Must be a positive u64. */ amount: bigint - /** Pool rebalancer whose ATA for `tokenAddress` must hold `amount` and delegate it to the pool signer. Defaults to `payer`. */ + /** Pool rebalancer whose ATA for `tokenAddress` must hold `amount`. Defaults to `payer`. */ authority?: string + /** Add an SPL Token approval for the pool signer before providing liquidity in the same transaction. */ + includeApproval?: boolean } type ParsedProvideLiquidityParams = { @@ -47,6 +50,7 @@ type ParsedProvideLiquidityParams = { poolProgram: PublicKey payer: PublicKey authority: PublicKey + includeApproval: boolean } /** Parameters for unsigned Solana lock-release pool liquidity provision. */ @@ -85,6 +89,7 @@ export class ProvideLiquidity extends SolanaOperation< params.authority === undefined ? payer : parsePublicKey(this.name, 'authority', params.authority), + includeApproval: params.includeApproval ?? false, } } @@ -120,13 +125,15 @@ export class ProvideLiquidity extends SolanaOperation< ) // The pool signer transfers from the rebalancer ATA as its SPL Token delegate. - validateDelegation( - this.name, - remoteTokenAccount, - remoteTokenAccountInfo, - poolSigner, - opts.amount, - ) + if (!opts.includeApproval) { + validateDelegation( + this.name, + remoteTokenAccount, + remoteTokenAccountInfo, + poolSigner, + opts.amount, + ) + } // The pool vault ATA must have been created during pool initialization. const { tokenAccount: poolTokenAccount } = await resolveExistingTokenAccount( @@ -135,7 +142,11 @@ export class ProvideLiquidity extends SolanaOperation< poolSigner, ) - const instruction = await createLockReleaseTokenPoolProgram(chain, opts.poolProgram, opts.payer) + const provideLiquidityInstruction = await createLockReleaseTokenPoolProgram( + chain, + opts.poolProgram, + opts.payer, + ) .methods.provideLiquidity(new BN(opts.amount.toString())) .accountsStrict({ state: deriveTokenPoolConfigPda(opts.poolProgram, opts.tokenAddress), @@ -148,6 +159,16 @@ export class ProvideLiquidity extends SolanaOperation< }) .instruction() + const approval = opts.includeApproval + ? await new ApproveToken().generate(chain, { + payer: opts.payer.toBase58(), + tokenAddress: opts.tokenAddress.toBase58(), + delegate: poolSigner.toBase58(), + amount: opts.amount, + authority: opts.authority.toBase58(), + }) + : undefined + chain.logger.debug( `${ this.name @@ -155,10 +176,11 @@ export class ProvideLiquidity extends SolanaOperation< opts.amount }`, ) + return { family: ChainFamily.Solana, - instructions: [instruction], - mainIndex: 0, + instructions: [...(approval?.instructions ?? []), provideLiquidityInstruction], + mainIndex: approval ? 1 : 0, } } From 6a4c2223500b2d63bdc49095264b26b1cf5bdd0f Mon Sep 17 00:00:00 2001 From: Mervin Date: Tue, 8 Sep 2026 15:35:26 +0800 Subject: [PATCH 82/87] feat(cct-sdk): Add createRecipientATA option to mint tokens op (#403) * feat: add transfer pool ownership op solana * feat: add accept pool ownership op solana * feat: add transfer authority op solana * fix: update export barrel * feat: add mint tokens op solana * fix: address comments * fix: address comments * feat: add set can accept liquidity op solana * fix: add lock release token pool idl * feat: add set rebalancer op solana * fix: update tsdoc * feat: add approve token op solana * feat: add provider liquidity op solana * fix: address comments * fix: revert unrelated changes * fix: add preflight checks * fix: update tsdoc * fix: extract validate pool liquidity config * feat: add withdraw liquidity op solana * fix: add preflight checks * feat: add update metadata authority op solana * fix: lint errors * fix: lint errors * fix: address comments * fix: refactor unit tests * fix: refactor export * feat: add owner override pending admint op solana * fix: update tsdoc and add preflight check * fix: update tsdoc @example * feat: add includeApproval option in provide liquidity * feat: add createRecipientATA option to mint tokens op * fix: address comments * fix: lint errors --- ccip-sdk/src/cct/solana/index.ts | 25 ++++---- .../token/operations/mint-tokens.test.ts | 27 ++++++++ .../solana/token/operations/mint-tokens.ts | 62 +++++++++++++++---- 3 files changed, 88 insertions(+), 26 deletions(-) diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index c7694a525..35962e80c 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -434,19 +434,18 @@ export class SolanaTokenManager extends TokenManager } /** - * Builds unsigned instructions to mint SPL tokens to a recipient's existing associated token account. + * Builds unsigned instructions to mint SPL tokens to a recipient's associated token account. * * @remarks - * `amount` is in base units. The recipient ATA must already exist; use - * {@link generateUnsignedCreateTokenAccount} to create it. `authority` defaults to `payer`. For - * an SPL Token multisig authority, provide `multisigSigners` and collect member signatures - * externally. + * `amount` is in base units. Set `createRecipientATA` to create the recipient ATA idempotently + * before minting; otherwise it must already exist. `authority` defaults to `payer`. For an SPL + * Token multisig authority, provide `multisigSigners` and collect member signatures externally. * * @throws {@link CCTParamsInvalidError} If an address, amount, or multisig signer 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 CCIPTokenAccountNotFoundError} If the recipient ATA is missing; create it first - * with {@link generateUnsignedCreateTokenAccount}. + * @throws {@link CCIPTokenAccountNotFoundError} If the recipient ATA is missing and + * `createRecipientATA` is not set. * * @example * ```ts @@ -464,20 +463,20 @@ export class SolanaTokenManager extends TokenManager } /** - * Mints SPL tokens to a recipient's existing associated token account using the executing wallet. + * Mints SPL tokens to a recipient's associated token account using the executing wallet. * * @remarks - * `amount` is in base units. The recipient ATA must already exist; use {@link createTokenAccount} - * to create it. SPL Token multisig authorities require `multisigSigners` and external member - * signatures; use {@link generateUnsignedMintTokens}. + * `amount` is in base units. Set `createRecipientATA` to create the recipient ATA idempotently + * before minting; otherwise it must already exist. SPL Token multisig authorities require + * `multisigSigners` and external member signatures; use {@link generateUnsignedMintTokens}. * * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. * @throws {@link CCTParamsInvalidError} If an address, amount, or multisig signer is invalid, or * `authority` does not match the executing wallet. * @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 CCIPTokenAccountNotFoundError} If the recipient ATA is missing; create it first - * with {@link createTokenAccount}. + * @throws {@link CCIPTokenAccountNotFoundError} If the recipient ATA is missing and + * `createRecipientATA` is not set. * @throws {@link CCTTxFailedError} If simulation or the SPL Token program rejects the transaction. * * @example diff --git a/ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts index 26b4d420f..af6bace9b 100644 --- a/ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts +++ b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.test.ts @@ -109,6 +109,32 @@ describe('MintTokens (cct/solana)', () => { ) }) + it('creates a missing recipient ATA when requested', async () => { + const missingAtaChain = { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + connection: { + getAccountInfo: async (address: PublicKey) => + address.equals(TOKEN) ? { owner: TOKEN_PROGRAM_ID } : null, + }, + } as unknown as SolanaChain + + const unsigned = await new MintTokens().generate(missingAtaChain, { + payer: PAYER, + tokenAddress: TOKEN.toBase58(), + recipient: RECIPIENT.toBase58(), + amount: 1n, + createRecipientATA: true, + }) + + const ata = getAssociatedTokenAddressSync(TOKEN, RECIPIENT, true, TOKEN_PROGRAM_ID) + assert.equal(unsigned.instructions.length, 2) + assert.equal(unsigned.mainIndex, 1) + assert.equal(unsigned.instructions[0]!.data[0], 1) // CreateIdempotent + assert.equal(unsigned.instructions[0]!.keys[1]!.pubkey.toBase58(), ata.toBase58()) + assert.equal(unsigned.instructions[1]!.data[0], 7) // MintTo + assert.equal(unsigned.instructions[1]!.keys[1]!.pubkey.toBase58(), ata.toBase58()) + }) + it('rejects a missing recipient ATA before simulation', async () => { const missingAtaChain = { logger: { debug() {}, info() {}, warn() {}, error() {} }, @@ -153,6 +179,7 @@ describe('MintTokens (cct/solana)', () => { [{ amount: 0n }, 'amount'], [{ amount: 1 }, 'amount'], [{ amount: U64_MAX + 1n }, 'amount'], + [{ createRecipientATA: 'invalid' }, 'createRecipientATA'], [{ multisigSigners: 'invalid' }, 'multisigSigners'], [{ multisigSigners: ['invalid'] }, 'multisigSigners[0]'], ] as const) { diff --git a/ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts index 26470f990..efdf3f2c5 100644 --- a/ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts +++ b/ccip-sdk/src/cct/solana/token/operations/mint-tokens.ts @@ -4,6 +4,7 @@ import type { PublicKey, TransactionInstruction } 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 { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' import { @@ -19,14 +20,12 @@ import { validateAuthorityMatchesWallet, validateBigInt, } from '../../validate.ts' +import { CreateTokenAccount } from './create-token-account.ts' type MintTokensParams = { /** SPL token mint address. */ tokenAddress: string - /** - * Associated Token Account (ATA) address for the recipient on this token mint. - * ⚠️ ATA must already exist; use `createTokenAccount` if needed. - */ + /** Recipient owner address; its ATA must exist unless `createRecipientATA` is set. */ recipient: string /** * Amount to mint in base units (not human-readable tokens). @@ -34,6 +33,8 @@ type MintTokensParams = { * Maximum u64: 2^64 - 1. */ amount: bigint + /** Create the recipient ATA idempotently before minting. Defaults to false. */ + createRecipientATA?: boolean /** Mint authority. Defaults to `payer` for single-signer transactions. */ authority?: string /** SPL Token multisig member addresses. Required when authority is an SPL Token multisig. */ @@ -41,9 +42,11 @@ type MintTokensParams = { } type ParsedMintTokensParams = { + payer: PublicKey tokenAddress: PublicKey recipient: PublicKey amount: bigint + createRecipientATA: boolean authority: PublicKey multisigSigners: PublicKey[] } @@ -60,7 +63,7 @@ export type ExecuteMintTokensParams = SolanaExecuteParams /** Result of executing Solana SPL token minting. */ export type ExecuteMintTokensResult = TransactionResult -/** Mints SPL tokens to a recipient's existing associated token account. */ +/** Mints SPL tokens to a recipient's associated token account. */ export class MintTokens extends SolanaOperation< MintTokensParams, UnsignedSolanaTx, @@ -74,12 +77,17 @@ export class MintTokens extends SolanaOperation< if (params.multisigSigners !== undefined && !Array.isArray(params.multisigSigners)) { throw new CCTParamsInvalidError(this.name, 'multisigSigners', 'must be an array') } + if (params.createRecipientATA !== undefined && typeof params.createRecipientATA !== 'boolean') { + throw new CCTParamsInvalidError(this.name, 'createRecipientATA', 'must be a boolean') + } const payer = parsePublicKey(this.name, 'payer', params.payer) return { + payer, tokenAddress: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), recipient: parsePublicKey(this.name, 'recipient', params.recipient), amount: params.amount, + createRecipientATA: params.createRecipientATA ?? false, authority: params.authority === undefined ? payer @@ -90,18 +98,38 @@ export class MintTokens extends SolanaOperation< } } - /** Builds an SPL Token `MintTo` instruction for the recipient's associated token account. */ + /** Builds recipient ATA creation, when requested, followed by an SPL Token `MintTo` instruction. */ protected async buildUnsigned( chain: SolanaChain, opts: ParsedMintTokensParams, ): Promise { - const { tokenAccount, tokenProgram } = await resolveExistingTokenAccount( - chain.connection, - opts.tokenAddress, - opts.recipient, - ) + const createRecipientATA = opts.createRecipientATA + ? await new CreateTokenAccount().generate(chain, { + payer: opts.payer.toBase58(), + tokenAddress: opts.tokenAddress.toBase58(), + ownerAddress: opts.recipient.toBase58(), + }) + : undefined + + let tokenAccount: PublicKey + let tokenProgram: PublicKey + + if (opts.createRecipientATA) { + const resolved = await resolveATA(chain.connection, opts.tokenAddress, opts.recipient) + tokenAccount = resolved.ata + tokenProgram = resolved.tokenProgram + } else { + const existing = await resolveExistingTokenAccount( + chain.connection, + opts.tokenAddress, + opts.recipient, + ) + tokenAccount = existing.tokenAccount + tokenProgram = existing.tokenProgram + } const instructions: TransactionInstruction[] = [ + ...(createRecipientATA?.instructions ?? []), createMintToInstruction( opts.tokenAddress, tokenAccount, @@ -113,9 +141,17 @@ export class MintTokens extends SolanaOperation< ] chain.logger.debug( - `${this.name}: token = ${opts.tokenAddress.toBase58()}, recipient = ${opts.recipient.toBase58()}, amount = ${opts.amount}`, + `${ + this.name + }: token = ${opts.tokenAddress.toBase58()}, recipient = ${opts.recipient.toBase58()}, amount = ${ + opts.amount + }`, ) - return { family: ChainFamily.Solana, instructions, mainIndex: 0 } + return { + family: ChainFamily.Solana, + instructions, + mainIndex: createRecipientATA ? 1 : 0, + } } /** Generate, sign, simulate, send, and confirm with the mint authority wallet. */ From e113e4b91843351392aecea0ab874def23c53b3e Mon Sep 17 00:00:00 2001 From: Mervin Date: Tue, 8 Sep 2026 18:36:19 +0800 Subject: [PATCH 83/87] fix(cct-sdk): EVM bytecode lint errors and tests (#410) * fix: lint errors * fix: npm install * fix: update metadata authority --- ccip-cli/src/index.ts | 2 +- ccip-sdk/src/api/index.ts | 2 +- .../V2_0_0/burn-from-mint-token-pool.ts | 2 +- .../bytecode/V2_0_0/burn-mint-token-pool.ts | 2 +- .../V2_0_0/burn-with-from-mint-token-pool.ts | 2 +- .../bytecode/V2_0_0/cross-chain-token.ts | 2 +- .../bytecode/V2_0_0/erc20-lockbox.ts | 2 +- .../V2_0_0/lock-release-token-pool.ts | 2 +- .../operations/update-metadata-authority.ts | 19 ++++++++-- ccip-sdk/src/solana/__tests__/index.test.ts | 6 ++- .../idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts | 4 +- package-lock.json | 37 ++++++++++++------- 12 files changed, 54 insertions(+), 28 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index f313f6a4f..fc0df1ae5 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -31,7 +31,7 @@ Error.stackTraceLimit = 50 // show more stack frames for better debugging // generate:nofail // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -const VERSION = '1.13.1-e6a58224' +const VERSION = '1.13.1-247aa263' // generate:end const require = createRequire(import.meta.url) diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index f1f681062..aea5ec973 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -63,7 +63,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.13.1-e6a58224' +export const SDK_VERSION = '1.13.1-247aa263' // generate:end /** SDK telemetry header name */ 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 index d45c91420..48cd4ad75 100644 --- 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 @@ -1,4 +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` +// `'${require('fs').readFileSync(require('module').createRequire(`${process.cwd()}/package.json`).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 index a00f5f881..d8a28816a 100644 --- 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 @@ -1,4 +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` +// `'${require('fs').readFileSync(require('module').createRequire(`${process.cwd()}/package.json`).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 index b109a6872..f587ebf7c 100644 --- 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 @@ -1,4 +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` +// `'${require('fs').readFileSync(require('module').createRequire(`${process.cwd()}/package.json`).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 index 52ca7b625..90d44e7c4 100644 --- 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 @@ -1,4 +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` +// `'${require('fs').readFileSync(require('module').createRequire(`${process.cwd()}/package.json`).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 index e69f8fd59..898d3cbd1 100644 --- 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 @@ -1,4 +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` +// `'${require('fs').readFileSync(require('module').createRequire(`${process.cwd()}/package.json`).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 index fbc5dcd89..aca19d5f1 100644 --- 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 @@ -1,4 +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` +// `'${require('fs').readFileSync(require('module').createRequire(`${process.cwd()}/package.json`).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/solana/token/operations/update-metadata-authority.ts b/ccip-sdk/src/cct/solana/token/operations/update-metadata-authority.ts index 2d713ae18..b33b77388 100644 --- a/ccip-sdk/src/cct/solana/token/operations/update-metadata-authority.ts +++ b/ccip-sdk/src/cct/solana/token/operations/update-metadata-authority.ts @@ -41,7 +41,9 @@ function validateMetadataAuthority( throw new CCTParamsInvalidError( operation, 'authority', - `${authority.toBase58()} is not the current metadata update authority (${metadata.updateAuthority})`, + `${authority.toBase58()} is not the current metadata update authority (${ + metadata.updateAuthority + })`, ) } if (!metadata.isMutable) { @@ -98,8 +100,9 @@ async function getMetadata( ) } + let parsed: MetadataAccountData try { - return metaplex.getMetadataAccountDataSerializer().deserialize(metadata.data)[0] + parsed = metaplex.getMetadataAccountDataSerializer().deserialize(metadata.data)[0] } catch { throw new CCTParamsInvalidError( operation, @@ -107,6 +110,14 @@ async function getMetadata( 'mint not found or does not have Metaplex metadata', ) } + if (!new PublicKey(parsed.mint).equals(tokenAddress)) { + throw new CCTParamsInvalidError( + operation, + 'tokenAddress', + 'mint not found or does not have Metaplex metadata', + ) + } + return parsed } /** Transfers the Metaplex metadata update authority for an SPL token mint. */ @@ -164,7 +175,9 @@ export class UpdateMetadataAuthority extends SolanaOperation< .map(metaplex.toWeb3JsInstruction) chain.logger.debug( - `${this.name}: token = ${opts.tokenAddress.toBase58()}, newAuthority = ${opts.newAuthority.toBase58()}`, + `${ + this.name + }: token = ${opts.tokenAddress.toBase58()}, newAuthority = ${opts.newAuthority.toBase58()}`, ) return { family: ChainFamily.Solana, instructions, mainIndex: 0 } } diff --git a/ccip-sdk/src/solana/__tests__/index.test.ts b/ccip-sdk/src/solana/__tests__/index.test.ts index 7dfbc64cd..321666b7a 100644 --- a/ccip-sdk/src/solana/__tests__/index.test.ts +++ b/ccip-sdk/src/solana/__tests__/index.test.ts @@ -4,7 +4,11 @@ import { beforeEach, describe, it, mock } from 'node:test' import { BorshAccountsCoder } from '@coral-xyz/anchor' import { type Connection, PublicKey } from '@solana/web3.js' -import { CCIPDataFormatUnsupportedError, CCIPCommitHistoryPrunedError, CCIPCommitNotFoundError } from '../../errors/index.ts' +import { + CCIPCommitHistoryPrunedError, + CCIPCommitNotFoundError, + CCIPDataFormatUnsupportedError, +} from '../../errors/index.ts' import { type NetworkInfo, ChainFamily, NetworkType } from '../../networks.ts' import { CCIPVersion } from '../../types.ts' import { type SolanaTransaction, SolanaChain } from '../index.ts' diff --git a/ccip-sdk/src/solana/idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts b/ccip-sdk/src/solana/idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts index 29cf17a06..aee4794fb 100644 --- a/ccip-sdk/src/solana/idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts +++ b/ccip-sdk/src/solana/idl/1.6.0/LOCK_RELEASE_TOKEN_POOL.ts @@ -3,7 +3,7 @@ // .then((res) => res.text()) // .then((text) => text.trim()) export type LockreleaseTokenPool = { - version: '1.6.3' + version: '1.6.4' name: 'lockrelease_token_pool' instructions: [ { @@ -987,7 +987,7 @@ export type LockreleaseTokenPool = { } export const IDL: LockreleaseTokenPool = { - version: '1.6.3', + version: '1.6.4', name: 'lockrelease_token_pool', instructions: [ { diff --git a/package-lock.json b/package-lock.json index 8959bb61c..d9e064593 100644 --- a/package-lock.json +++ b/package-lock.json @@ -175,18 +175,6 @@ } } }, - "ccip-sdk/node_modules/axios": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", - "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.6", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" - } - }, "ccip-sdk/node_modules/typescript": { "version": "7.0.2", "dev": true, @@ -7000,6 +6988,23 @@ "react-dom": "19.1.4" } }, + "node_modules/@ledgerhq/domain-service/node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/@ledgerhq/domain-service/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/@ledgerhq/domain-service/node_modules/react": { "version": "19.1.4", "license": "MIT", @@ -11212,7 +11217,9 @@ } }, "node_modules/axios": { - "version": "1.19.0", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", @@ -14786,7 +14793,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.5", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "funding": [ { "type": "github", From eb2fd9f439526e7cbb1df3b2520a4e5afff937f1 Mon Sep 17 00:00:00 2001 From: Mervin Date: Tue, 8 Sep 2026 20:26:18 +0800 Subject: [PATCH 84/87] feat(cct-sdk): Add get token info op solana (#405) * feat: add transfer pool ownership op solana * feat: add accept pool ownership op solana * feat: add transfer authority op solana * fix: update export barrel * feat: add mint tokens op solana * fix: address comments * fix: address comments * feat: add set can accept liquidity op solana * fix: add lock release token pool idl * feat: add set rebalancer op solana * fix: update tsdoc * feat: add approve token op solana * feat: add provider liquidity op solana * fix: address comments * fix: revert unrelated changes * fix: add preflight checks * fix: update tsdoc * fix: extract validate pool liquidity config * feat: add withdraw liquidity op solana * fix: add preflight checks * feat: add update metadata authority op solana * fix: lint errors * fix: lint errors * fix: address comments * fix: refactor unit tests * fix: refactor export * feat: add owner override pending admint op solana * fix: update tsdoc and add preflight check * fix: update tsdoc @example * feat: add includeApproval option in provide liquidity * feat: add createRecipientATA option to mint tokens op * feat: add get token info op solana * fix: address comments * fix: package lock file - remove stale axios version * feat(cct-sdk):Add createPoolSignerATA option to deploy token pool (#412) * feat: add createPoolSignerATA option to deploy token pool op * fix: address comments --- ccip-sdk/src/cct/solana/index.test.ts | 2 + ccip-sdk/src/cct/solana/index.ts | 73 ++++++++++++++-- .../operations/deploy-token-pool.test.ts | 43 ++++++++++ .../operations/deploy-token-pool.ts | 35 +++++++- .../token/operations/get-token-info.test.ts | 86 +++++++++++++++++++ .../solana/token/operations/get-token-info.ts | 84 ++++++++++++++++++ .../src/cct/solana/token/operations/index.ts | 1 + ccip-sdk/src/gas.ts | 3 +- package-lock.json | 17 ---- 9 files changed, 320 insertions(+), 24 deletions(-) create mode 100644 ccip-sdk/src/cct/solana/token/operations/get-token-info.test.ts create mode 100644 ccip-sdk/src/cct/solana/token/operations/get-token-info.ts diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index 9f0551a7c..c575e790d 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -197,6 +197,7 @@ describe('SolanaTokenManager (cct/solana)', () => { getTokenAdminRegistryFor: async (address: string) => address === reader ? pool : address === overrideAddress ? overrideRouter : account, getSupportedTokens: async () => [mint], + getTokenInfo: async () => ({ symbol: 'TKN', decimals: 6 }), getTokenPoolRemotes: async () => ({}), getRegistryTokenConfig: async (router: string) => router === overrideRouter @@ -779,6 +780,7 @@ describe('SolanaTokenManager (cct/solana)', () => { remoteChainSelector, }), ], + ['getTokenInfo', () => cct.getTokenInfo({ tokenAddress: mint })], [ 'getTokenPoolState', () => diff --git a/ccip-sdk/src/cct/solana/index.ts b/ccip-sdk/src/cct/solana/index.ts index 35962e80c..a4f634beb 100644 --- a/ccip-sdk/src/cct/solana/index.ts +++ b/ccip-sdk/src/cct/solana/index.ts @@ -177,8 +177,11 @@ import { type GenerateSetTokenAuthorityResult, type GenerateUpdateMetadataAuthorityParams, type GenerateUpdateMetadataAuthorityResult, + type GetTokenInfoParams, + type GetTokenInfoResult, ApproveToken, CreateTokenAccount, + GetTokenInfo, MintTokens, SetTokenAuthority, UpdateMetadataAuthority, @@ -190,6 +193,7 @@ export class SolanaTokenManager extends TokenManager // Token operations readonly #approveToken = new ApproveToken() readonly #createTokenAccount = new CreateTokenAccount() + readonly #getTokenInfo = new GetTokenInfo() readonly #mintTokens = new MintTokens() readonly #setTokenAuthority = new SetTokenAuthority() readonly #updateMetadataAuthority = new UpdateMetadataAuthority() @@ -818,12 +822,25 @@ export class SolanaTokenManager extends TokenManager * @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`. + * must be allowed to initialize the pool. + * + * **Important:** The pool requires a `pool_token_account` (the pool signer PDA's associated token + * account) to lock/release or mint on transfers. Set `createPoolSignerATA: true` to create it + * idempotently in this transaction. If omitted (defaults to `false`), create it separately with + * the returned `poolSignerAddress` via `generateUnsignedCreateTokenAccount` before + * `generateUnsignedSetPool`, or transfers fail with `AccountNotInitialized (3012)`. + * + * When to use `createPoolSignerATA: true` vs. the separate + * `generateUnsignedCreateTokenAccount` op: + * - Use this option when deploying a pool that will immediately receive transfers (simplest, one tx) + * - Use the separate op for vault-owned pools or when decoupling pool initialization from ATA setup + * + * This option is Solana-only (no EVM equivalent). It is analogous to `createRecipientATA` on + * {@link mintTokens}, the same idiomatic pattern for atomicity. * * @see {@link generateUnsignedCreateTokenAccount} * @see {@link generateUnsignedSetPool} + * @see {@link mintTokens} * * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid. * @@ -836,6 +853,7 @@ export class SolanaTokenManager extends TokenManager * payer, * authority, * allowlist: [allowedSender], + * createPoolSignerATA: true, * }) * ``` */ @@ -851,11 +869,25 @@ export class SolanaTokenManager extends TokenManager * @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`. + * must be allowed to initialize the pool. + * + * **Important:** The pool requires a `pool_token_account` (the pool signer PDA's associated token + * account) to lock/release or mint on transfers. Set `createPoolSignerATA: true` to create it + * idempotently in this transaction. If omitted (defaults to `false`), create it separately with + * the returned `poolSignerAddress` via `createTokenAccount` before `setPool`, or transfers fail + * with `AccountNotInitialized (3012)`. + * + * When to use `createPoolSignerATA: true` vs. the separate + * `generateUnsignedCreateTokenAccount` op: + * - Use this option when deploying a pool that will immediately receive transfers (simplest, one tx) + * - Use the separate op for vault-owned pools or when decoupling pool initialization from ATA setup + * + * This option is Solana-only (no EVM equivalent). It is analogous to `createRecipientATA` on + * {@link mintTokens}, the same idiomatic pattern for atomicity. * * @see {@link createTokenAccount} * @see {@link setPool} + * @see {@link mintTokens} * * @throws {@link CCIPWalletInvalidError} If `wallet` cannot sign Solana transactions. * @throws {@link CCTParamsInvalidError} If a pool parameter is invalid. @@ -867,6 +899,7 @@ export class SolanaTokenManager extends TokenManager * await cct.deployTokenPool({ * tokenAddress: mint, * poolType: 'burn-mint', + * createPoolSignerATA: true, * wallet, * }) * ``` @@ -2230,6 +2263,36 @@ export class SolanaTokenManager extends TokenManager return this.#transferAdmin.execute(this.chain, opts) } + /** + * Reads an SPL token mint's metadata, program, supply, initialization state, and mint/freeze + * authorities. + * + * @remarks Metadata comes from {@link SolanaChain.getTokenInfo}; mint state comes directly from + * the SPL Token or Token-2022 mint account. Supply is in base units. Solana-only; no EVM CCT + * equivalent exists. + * + * @see {@link setTokenAuthority} Sets the mint or freeze authorities returned here. + * @see {@link updateMetadataAuthority} Updates the Metaplex metadata associated with this mint. + * @see {@link getTokenPoolState} Reads pool configuration rather than mint state. + * @see {@link SolanaChain.getTokenInfo} Reads the underlying token metadata. + * + * @throws {@link CCTParamsInvalidError} If `tokenAddress` is not a valid Solana public key. + * @throws {@link CCIPSplTokenInvalidError} If the token metadata is not a valid SPL token. + * @throws {@link CCIPTokenMintNotFoundError} If the mint account does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * @throws {@link CCIPTokenDataParseError} If the mint data cannot be parsed. + * + * @example + * ```ts + * const cct = SolanaTokenManager.fromChain(chain) + * const info = await cct.getTokenInfo({ tokenAddress: mint }) + * console.log(`${info.symbol}: ${info.decimals} decimals`) + * ``` + */ + getTokenInfo(opts: GetTokenInfoParams): Promise { + return this.#getTokenInfo.query(this.chain, opts) + } + /** * Reads all, or one selected, Solana token pool remote-chain configurations. * 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 index d45ba8b87..42cde3de0 100644 --- 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 @@ -1,6 +1,11 @@ import assert from 'node:assert/strict' import { describe, it } from 'node:test' +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, +} from '@solana/spl-token' import { Keypair, PublicKey } from '@solana/web3.js' import { ChainFamily } from '../../../../networks.ts' @@ -72,6 +77,36 @@ describe('DeployTokenPool (cct/solana)', () => { assert.equal(unsigned.instructions[1]!.programId.toBase58(), BURN_MINT_POOL_PROGRAM) }) + it('creates the pool signer ATA when requested', async () => { + const chain = Object.assign(stubChain(), { + connection: { getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID }) }, + }) + const unsigned = await new DeployTokenPool().generate(chain, { + tokenAddress: TOKEN, + poolType: 'burn-mint', + payer: PAYER, + authority: AUTHORITY, + createPoolSignerATA: true, + }) + const poolSigner = deriveTokenPoolSignerPda( + resolveTokenPoolProgram('burn-mint'), + new PublicKey(TOKEN), + ) + const ata = getAssociatedTokenAddressSync( + new PublicKey(TOKEN), + poolSigner, + true, + TOKEN_PROGRAM_ID, + ) + const createATA = unsigned.instructions[1]! + + assert.equal(unsigned.instructions.length, 2) + assert.equal(createATA.programId.toBase58(), ASSOCIATED_TOKEN_PROGRAM_ID.toBase58()) + assert.equal(createATA.data[0], 1) // CreateIdempotent + assert.equal(createATA.keys[1]!.pubkey.toBase58(), ata.toBase58()) + assert.equal(createATA.keys[2]!.pubkey.toBase58(), poolSigner.toBase58()) + }) + it('uses canonical lock-release pool program', async () => { const unsigned = await generate({ poolType: 'lock-release' }) @@ -93,6 +128,14 @@ describe('DeployTokenPool (cct/solana)', () => { ) }) + it('rejects a non-boolean createPoolSignerATA', async () => { + await assert.rejects( + () => generate({ createPoolSignerATA: 'yes' }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'createPoolSignerATA', + ) + }) + it('rejects invalid pool types', async () => { await assert.rejects( () => generate({ poolType: 'custom' }), 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 index 13f7684d2..23c6388af 100644 --- 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 @@ -20,6 +20,7 @@ import { resolveTokenPoolProgram, } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' +import { CreateTokenAccount } from '../../token/operations/create-token-account.ts' import { parsePublicKey, validateAuthorityMatchesWallet, validatePoolType } from '../../validate.ts' /** @@ -42,6 +43,17 @@ type DeployTokenPoolParams = { * If omitted, the pool is initialized without an allowlist. */ allowlist?: string[] + /** + * Create the pool signer PDA's associated token account (`pool_token_account`) idempotently. + * + * @remarks The pool requires a `pool_token_account` (the associated token account owned by the + * `pool_signer` PDA) to lock/release or mint on transfers. Without it, a `ccip-send` fails with + * `AccountNotInitialized (3012)`. Setting `createPoolSignerATA: true` creates this account in the + * deploy transaction; otherwise create it separately before any transfer. + * + * Defaults to false. + */ + createPoolSignerATA?: boolean /** Pool authority. Defaults to payer for unsigned generation and wallet public key for execute. */ authority?: string } @@ -55,6 +67,7 @@ type ParsedDeployTokenPoolParams = { payer: PublicKey authority: PublicKey allowlist: PublicKey[] + createPoolSignerATA: boolean } /** Unsigned Solana token pool deploy result plus derived pool PDAs. */ @@ -86,6 +99,12 @@ export class DeployTokenPool extends SolanaOperation< if (params.allowlist !== undefined && !Array.isArray(params.allowlist)) { throw new CCTParamsInvalidError(this.name, 'allowlist', 'must be an array') } + if ( + params.createPoolSignerATA !== undefined && + typeof params.createPoolSignerATA !== 'boolean' + ) { + throw new CCTParamsInvalidError(this.name, 'createPoolSignerATA', 'must be a boolean') + } const payer = parsePublicKey(this.name, 'payer', params.payer) return { @@ -99,6 +118,7 @@ export class DeployTokenPool extends SolanaOperation< allowlist: (params.allowlist ?? []).map((address, i) => parsePublicKey(this.name, `allowlist[${i}]`, address), ), + createPoolSignerATA: params.createPoolSignerATA ?? false, } } @@ -107,7 +127,7 @@ export class DeployTokenPool extends SolanaOperation< chain: SolanaChain, opts: ParsedDeployTokenPoolParams, ): Promise { - const { tokenMint, poolProgram, payer, authority, allowlist } = opts + const { tokenMint, poolProgram, payer, authority, allowlist, createPoolSignerATA } = opts const program = createTokenPoolProgram(chain, poolProgram, payer) const state = deriveTokenPoolConfigPda(poolProgram, tokenMint) const poolSigner = deriveTokenPoolSignerPda(poolProgram, tokenMint) @@ -127,6 +147,19 @@ export class DeployTokenPool extends SolanaOperation< .instruction(), ] + if (createPoolSignerATA) { + // Append ATA creation after initialize (initialize must be main instruction index 0) + instructions.push( + ...( + await new CreateTokenAccount().generate(chain, { + payer: payer.toBase58(), + tokenAddress: tokenMint.toBase58(), + ownerAddress: poolSigner.toBase58(), + }) + ).instructions, + ) + } + if (allowlist.length) { instructions.push( await program.methods diff --git a/ccip-sdk/src/cct/solana/token/operations/get-token-info.test.ts b/ccip-sdk/src/cct/solana/token/operations/get-token-info.test.ts new file mode 100644 index 000000000..b127eec27 --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/get-token-info.test.ts @@ -0,0 +1,86 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { MINT_SIZE, MintLayout, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { Keypair, PublicKey } from '@solana/web3.js' + +import { CCIPTokenDataParseError } from '../../../../errors/index.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { GetTokenInfo } from './get-token-info.ts' + +const tokenAddress = Keypair.generate().publicKey.toBase58() +const mintAuthority = Keypair.generate().publicKey + +function mintData(): Buffer { + const data = Buffer.alloc(MINT_SIZE) + MintLayout.encode( + { + mintAuthorityOption: 1, + mintAuthority, + supply: 1_000_000n, + decimals: 6, + isInitialized: true, + freezeAuthorityOption: 0, + freezeAuthority: PublicKey.default, + }, + data, + ) + return data +} + +describe('GetTokenInfo (cct/solana)', () => { + describe('query', () => { + it('delegates metadata to SolanaChain.getTokenInfo and reads mint state', async () => { + const metadata = { symbol: 'TKN', decimals: 6, name: 'Token' } + let received: string | undefined + const chain = { + connection: { + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID, data: mintData() }), + }, + getTokenInfo: async (token: string) => { + received = token + return metadata + }, + } as unknown as SolanaChain + + assert.deepEqual(await new GetTokenInfo().query(chain, { tokenAddress }), { + ...metadata, + tokenProgram: TOKEN_PROGRAM_ID.toBase58(), + supply: 1_000_000n, + isInitialized: true, + mintAuthority: mintAuthority.toBase58(), + freezeAuthority: null, + }) + assert.equal(received, tokenAddress) + }) + + it('rejects non-mint SPL accounts before fetching metadata', async () => { + let metadataCalls = 0 + const chain = { + connection: { + getAccountInfo: async () => ({ owner: TOKEN_PROGRAM_ID, data: Buffer.alloc(1) }), + }, + getTokenInfo: async () => { + metadataCalls++ + return { symbol: 'TKN', decimals: 6 } + }, + } as unknown as SolanaChain + + await assert.rejects(new GetTokenInfo().query(chain, { tokenAddress }), (error: unknown) => { + assert.ok(error instanceof CCIPTokenDataParseError) + assert.equal(error.context.token, tokenAddress) + assert.ok(error.cause instanceof Error) + return true + }) + assert.equal(metadataCalls, 0) + }) + }) + + describe('validation', () => { + it('validates the mint address before querying', async () => { + await assert.rejects( + new GetTokenInfo().query({} as SolanaChain, { tokenAddress: 'not-a-public-key' }), + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/solana/token/operations/get-token-info.ts b/ccip-sdk/src/cct/solana/token/operations/get-token-info.ts new file mode 100644 index 000000000..70579e7cf --- /dev/null +++ b/ccip-sdk/src/cct/solana/token/operations/get-token-info.ts @@ -0,0 +1,84 @@ +import { unpackMint } from '@solana/spl-token' +import type { PublicKey } from '@solana/web3.js' + +import type { TokenInfo } from '../../../../chain.ts' +import { CCIPTokenDataParseError } from '../../../../errors/index.ts' +import type { SolanaChain } from '../../../../solana/index.ts' +import { resolveTokenMint } from '../../../../solana/utils.ts' +import { SolanaQuery } from '../../query.ts' +import { parsePublicKey } from '../../validate.ts' + +/** Parameters for reading an SPL token mint's metadata. */ +export type GetTokenInfoParams = { + /** SPL token mint address. */ + tokenAddress: string +} + +/** SPL token metadata and mint state. */ +export type GetTokenInfoResult = TokenInfo & { + /** SPL Token or Token-2022 program that owns the mint. */ + tokenProgram: string + /** Total minted supply in base units. */ + supply: bigint + /** Whether the mint account has been initialized. */ + isInitialized: boolean + /** Authority allowed to mint new tokens, or null for fixed-supply tokens. */ + mintAuthority: string | null + /** Authority allowed to freeze token accounts, or null when freezing is disabled. */ + freezeAuthority: string | null +} + +type ParsedGetTokenInfoParams = GetTokenInfoParams & { mint: PublicKey } + +/** + * Reads an SPL token mint's metadata and state. + * + * @throws {@link CCTParamsInvalidError} If `tokenAddress` is not a valid Solana public key. + * @throws {@link CCIPSplTokenInvalidError} If the token metadata is not a valid SPL token. + * @throws {@link CCIPTokenMintNotFoundError} If the mint account does not exist. + * @throws {@link CCIPTokenMintInvalidError} If the mint is not owned by an SPL Token program. + * @throws {@link CCIPTokenDataParseError} If the mint data cannot be parsed. + */ +export class GetTokenInfo extends SolanaQuery< + GetTokenInfoParams, + GetTokenInfoResult, + ParsedGetTokenInfoParams +> { + readonly name = 'getTokenInfo' + + /** Converts and validates the mint address. */ + protected prepare(params: GetTokenInfoParams): ParsedGetTokenInfoParams { + return { + ...params, + mint: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), + } + } + + /** Delegates metadata lookup and reads the mint's SPL state. */ + protected async read( + chain: SolanaChain, + { mint, tokenAddress }: ParsedGetTokenInfoParams, + ): Promise { + const account = await resolveTokenMint(chain.connection, mint) + + let state + try { + state = unpackMint(mint, account, account.owner) + } catch (cause) { + throw new CCIPTokenDataParseError(tokenAddress, { + cause: cause instanceof Error ? cause : undefined, + }) + } + + const info = await chain.getTokenInfo(tokenAddress) + + return { + ...info, + tokenProgram: account.owner.toBase58(), + supply: state.supply, + isInitialized: state.isInitialized, + mintAuthority: state.mintAuthority?.toBase58() ?? null, + freezeAuthority: state.freezeAuthority?.toBase58() ?? null, + } + } +} diff --git a/ccip-sdk/src/cct/solana/token/operations/index.ts b/ccip-sdk/src/cct/solana/token/operations/index.ts index ac2fc6043..6c8f9477d 100644 --- a/ccip-sdk/src/cct/solana/token/operations/index.ts +++ b/ccip-sdk/src/cct/solana/token/operations/index.ts @@ -1,6 +1,7 @@ export * from './approve-token.ts' export * from './create-token-account.ts' export * from './deploy-token.ts' +export * from './get-token-info.ts' export * from './mint-tokens.ts' export * from './set-token-authority.ts' export * from './update-metadata-authority.ts' diff --git a/ccip-sdk/src/gas.ts b/ccip-sdk/src/gas.ts index 957177c41..738da11ae 100644 --- a/ccip-sdk/src/gas.ts +++ b/ccip-sdk/src/gas.ts @@ -196,7 +196,8 @@ export async function getDestTokenAmount({ let sourceTokenAddress, sourcePoolAddress, destTokenAddress if ('destTokenAddress' in tokenAmount) { ;({ destTokenAddress, sourcePoolAddress, sourceTokenAddress } = tokenAmount) - } else if (!source) return tokenAmount // if we don't have a source, assume we were already given a dest `{token, amount}` + } else if (!source) + return tokenAmount // if we don't have a source, assume we were already given a dest `{token, amount}` else { ;({ destTokenAddress, sourceTokenAddress, sourcePoolAddress } = await sourceToDestTokenAddresses({ diff --git a/package-lock.json b/package-lock.json index d9e064593..b236d1852 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6988,23 +6988,6 @@ "react-dom": "19.1.4" } }, - "node_modules/@ledgerhq/domain-service/node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/@ledgerhq/domain-service/node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/@ledgerhq/domain-service/node_modules/react": { "version": "19.1.4", "license": "MIT", From 19bb2a5713027b9ef445eddd4130bdf8088a8fe7 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:05:38 +0100 Subject: [PATCH 85/87] feat(cct-sdk): Add token mint and role reads (#406) --- ccip-sdk/src/cct/evm/index.test.ts | 106 +++++++ ccip-sdk/src/cct/evm/index.ts | 152 ++++++++++ ccip-sdk/src/cct/evm/token/contracts.ts | 129 +++++++- .../evm/token/operations/get-burners.test.ts | 94 ++++++ .../cct/evm/token/operations/get-burners.ts | 44 +++ .../evm/token/operations/get-minters.test.ts | 94 ++++++ .../cct/evm/token/operations/get-minters.ts | 44 +++ .../evm/token/operations/is-burner.test.ts | 94 ++++++ .../src/cct/evm/token/operations/is-burner.ts | 49 ++++ .../evm/token/operations/is-minter.test.ts | 94 ++++++ .../src/cct/evm/token/operations/is-minter.ts | 50 ++++ .../src/cct/evm/token/operations/mint.test.ts | 277 ++++++++++++++++++ ccip-sdk/src/cct/evm/token/operations/mint.ts | 95 ++++++ 13 files changed, 1317 insertions(+), 5 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/token/operations/get-burners.test.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/get-burners.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/get-minters.test.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/get-minters.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/is-burner.test.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/is-burner.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/is-minter.test.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/is-minter.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/mint.test.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/mint.ts diff --git a/ccip-sdk/src/cct/evm/index.test.ts b/ccip-sdk/src/cct/evm/index.test.ts index ff92a7131..f0ce135e2 100644 --- a/ccip-sdk/src/cct/evm/index.test.ts +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -722,4 +722,110 @@ describe('EVMTokenManager (cct/evm)', () => { assert.equal(called, false, 'validation fails before TAR discovery') }) }) + + describe('mint and role reads', () => { + const MINTER = '0x' + '99'.repeat(20) + const RECIPIENT = '0x' + 'aa'.repeat(20) + const AMOUNT = 1_000000000000000000n + /** Fresh Interface — the manager's own cached one must not be what these assertions compare to. */ + const TOKEN_FNS = new Interface([ + 'function mint(address account, uint256 amount)', + 'function isMinter(address minter) view returns (bool)', + 'function isBurner(address burner) view returns (bool)', + 'function getMinters() view returns (address[])', + 'function getBurners() view returns (address[])', + ]) + + /** Chain stub for a BurnMintERC677 token on which `MINTER` holds the mint role. */ + function tokenChain(isMinter = true) { + const results: Record = { + isMinter: [isMinter], + isBurner: [isMinter], + getMinters: [[MINTER]], + getBurners: [[POOL]], + } + return stubChain({ + provider: { + call: ({ data }: { data: string }) => { + const fn = TOKEN_FNS.getFunction(data.slice(0, 10))!.name + return Promise.resolve(TOKEN_FNS.encodeFunctionResult(fn, results[fn])) + }, + } as never, + }) + } + + it('generateUnsignedMint encodes mint(account, amount) to the token', async () => { + const cct = EVMTokenManager.fromChain(tokenChain()) + const unsigned = await cct.generateUnsignedMint({ + tokenAddress: TOKEN, + account: RECIPIENT, + amount: AMOUNT, + sender: MINTER, + }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal(tx.to, TOKEN) + assert.equal(tx.from, MINTER) + assert.equal(tx.data, TOKEN_FNS.encodeFunctionData('mint', [RECIPIENT, AMOUNT])) + }) + + it('mint submits as the minting wallet', async () => { + const cct = EVMTokenManager.fromChain(tokenChain()) + const { hash } = await cct.mint({ + tokenAddress: TOKEN, + account: RECIPIENT, + amount: AMOUNT, + wallet: fakeSigner(MINTER), + }) + assert.equal(hash, HASH) + }) + + it('mint rejects a wallet without the mint role', async () => { + const cct = EVMTokenManager.fromChain(tokenChain(false)) + await assert.rejects( + () => + cct.mint({ + tokenAddress: TOKEN, + account: RECIPIENT, + amount: AMOUNT, + wallet: fakeSigner(ADMIN), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('getMinters lists the mint-role holders', async () => { + const cct = EVMTokenManager.fromChain(tokenChain()) + assert.deepEqual(await cct.getMinters({ tokenAddress: TOKEN }), [MINTER]) + }) + + it('getBurners lists the burn-role holders', async () => { + const cct = EVMTokenManager.fromChain(tokenChain()) + assert.deepEqual(await cct.getBurners({ tokenAddress: TOKEN }), [POOL]) + }) + + it('isMinter answers the single-address mint-role check', async () => { + assert.equal( + await EVMTokenManager.fromChain(tokenChain()).isMinter({ + tokenAddress: TOKEN, + account: MINTER, + }), + true, + ) + assert.equal( + await EVMTokenManager.fromChain(tokenChain(false)).isMinter({ + tokenAddress: TOKEN, + account: RECIPIENT, + }), + false, + ) + }) + + it('isBurner answers the single-address burn-role check', async () => { + const cct = EVMTokenManager.fromChain(tokenChain()) + assert.equal(await cct.isBurner({ tokenAddress: TOKEN, account: POOL }), true) + }) + }) }) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 78905e5e1..48b11e18a 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -88,12 +88,30 @@ import { TransferOwnership, } from './token-pool/operations/transfer-ownership.ts' import { type DeployTokenParams, DeployToken } from './token/operations/deploy-token.ts' +import { + type GetBurnersParams, + type GetBurnersResult, + GetBurners, +} from './token/operations/get-burners.ts' +import { + type GetMintersParams, + type GetMintersResult, + GetMinters, +} from './token/operations/get-minters.ts' +import { type IsBurnerParams, type IsBurnerResult, IsBurner } from './token/operations/is-burner.ts' +import { type IsMinterParams, type IsMinterResult, IsMinter } from './token/operations/is-minter.ts' +import { type MintParams, Mint } from './token/operations/mint.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() + readonly #mint = new Mint() + readonly #getMinters = new GetMinters() + readonly #getBurners = new GetBurners() + readonly #isMinter = new IsMinter() + readonly #isBurner = new IsBurner() // Token admin registry operations readonly #registerAdmin = new RegisterAdmin() @@ -677,6 +695,135 @@ export class EVMTokenManager extends TokenManager { return this.#deployToken.execute(this.chain, opts) } + /** + * Builds an unsigned `mint` tx (for multisig / offline signing): mints new supply of a + * BurnMintERC677 token to `account`. The manual mint — seeding liquidity, topping up test + * supply — not the bridge path, which mints through the pool. + * @remarks v1.5.1 / v1.6.2 tokens only; v2.0.0's `CrossChainToken` gates minting through + * AccessControl, which ships separately. `sender` is checked against the token's + * `isMinter(address)`, **not** its owner: `mint` is `onlyMinter`, and the owner is the role + * admin, who need not hold the role. Grant it first with `grantMintRole`. The full sequence: + * {@link deployToken} → `grantMintRole` → {@link generateUnsignedMint}, checking the grant + * landed with {@link isMinter} (or {@link getMinters} for the whole set). + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, or `sender` is given and does + * not hold the token's mint role + * @example + * ```typescript + * // build only — sign later (multisig / offline). `sender` must hold the mint role. + * const unsigned = await cct.generateUnsignedMint({ + * tokenAddress: '0xToken...', + * account: '0xRecipient...', + * amount: 1_000_000000000000000000n, // 1000 tokens at 18 decimals + * sender: '0xMinter...', + * }) + * ``` + */ + generateUnsignedMint(opts: MintParams): Promise { + return this.#mint.generate(this.chain, opts) + } + + /** + * Mints new supply of a BurnMintERC677 token to `account`, signing + submitting with + * `opts.wallet` (an address holding the token's mint role). + * @remarks See {@link generateUnsignedMint} for the version and role rules. `sender` defaults + * to the wallet's address, so the mint-role check always runs before this submits. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the wallet's address, or the wallet does not hold the token's mint role + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain — e.g. the mint would + * exceed the token's `maxSupply`, which is not pre-flighted + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.mint({ + * tokenAddress: '0xToken...', + * account: '0xRecipient...', + * amount: 1_000_000000000000000000n, + * wallet, // must hold the mint role + * }) + * ``` + */ + mint(opts: EVMExecuteParams): Promise { + return this.#mint.execute(this.chain, opts) + } + + /** + * Lists every account holding a BurnMintERC677 token's mint role, via `getMinters()`. + * @remarks Informational, for audit and UX. To check *one* address, use {@link isMinter} — one + * call instead of an unbounded set plus a client-side scan. + * @remarks v1.5.1 / v1.6.2 tokens only: v2.0.0's `CrossChainToken` uses AccessControl, which + * does not enumerate role members, so there is no equivalent read. + * @throws {@link CCTParamsInvalidError} if `tokenAddress` is not a valid, non-zero address + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @example + * ```typescript + * const minters = await cct.getMinters({ tokenAddress: '0xToken...' }) + * console.log(minters) // ['0xPool...', '0xOpsKey...'] + * ``` + */ + getMinters(opts: GetMintersParams): Promise { + return this.#getMinters.query(this.chain, opts) + } + + /** + * Lists every account holding a BurnMintERC677 token's burn role, via `getBurners()`. + * @remarks Same shape and caveats as {@link getMinters}; to check one address, use + * {@link isBurner}. + * @throws {@link CCTParamsInvalidError} if `tokenAddress` is not a valid, non-zero address + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @example + * ```typescript + * const burners = await cct.getBurners({ tokenAddress: '0xToken...' }) + * ``` + */ + getBurners(opts: GetBurnersParams): Promise { + return this.#getBurners.query(this.chain, opts) + } + + /** + * Reads whether `account` holds a BurnMintERC677 token's mint role, via `isMinter(address)`. + * @remarks The pre-flight for a {@link mint}: the token's `mint` is `onlyMinter`, and the owner + * is only the role admin, who need not hold the role. Prefer this over scanning + * {@link getMinters} — one call, and it stays a single call as the role set grows. + * @throws {@link CCTParamsInvalidError} if `tokenAddress` or `account` is not a valid, non-zero + * address + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @example + * ```typescript + * if (await cct.isMinter({ tokenAddress: '0xToken...', account: '0xOpsKey...' })) { + * await cct.mint({ tokenAddress: '0xToken...', account: '0xRecipient...', amount, wallet }) + * } + * ``` + */ + isMinter(opts: IsMinterParams): Promise { + return this.#isMinter.query(this.chain, opts) + } + + /** + * Reads whether `account` holds a BurnMintERC677 token's burn role, via `isBurner(address)`. + * @remarks Same shape and caveats as {@link isMinter}; the burn-role counterpart of the set + * read {@link getBurners}. + * @throws {@link CCTParamsInvalidError} if `tokenAddress` or `account` is not a valid, non-zero + * address + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @example + * ```typescript + * const poolCanBurn = await cct.isBurner({ tokenAddress: '0xToken...', account: '0xPool...' }) + * ``` + */ + isBurner(opts: IsBurnerParams): Promise { + return this.#isBurner.query(this.chain, opts) + } + /** * Builds an unsigned pool deployment tx (for multisig / offline signing). `type` selects * the pool contract — a `DeployableTokenPoolType` (`BurnMintTokenPool`, `BurnFromMintTokenPool`, @@ -1287,6 +1434,11 @@ export type { } from './token-admin-registry/operations/get-supported-tokens.ts' export * from './token-admin-registry/contracts.ts' export type { DeployTokenParams } from './token/operations/deploy-token.ts' +export type { MintParams } from './token/operations/mint.ts' +export type { GetMintersParams, GetMintersResult } from './token/operations/get-minters.ts' +export type { GetBurnersParams, GetBurnersResult } from './token/operations/get-burners.ts' +export type { IsMinterParams, IsMinterResult } from './token/operations/is-minter.ts' +export type { IsBurnerParams, IsBurnerResult } from './token/operations/is-burner.ts' export * from './token/contracts.ts' export type { DeployTokenPoolParams, diff --git a/ccip-sdk/src/cct/evm/token/contracts.ts b/ccip-sdk/src/cct/evm/token/contracts.ts index cdd3aa180..81e823b2e 100644 --- a/ccip-sdk/src/cct/evm/token/contracts.ts +++ b/ccip-sdk/src/cct/evm/token/contracts.ts @@ -1,20 +1,26 @@ /** * 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`. + * ({@link getTokenInterface}) for read/write (e.g. ownership) ops, the deployable + * `CrossChainToken` (v2.0.0) artifact ({@link getTokenArtifact}), and the token's role reads — + * the narrow predicate a role-gated write pre-flights ({@link readTokenRole}) and the + * informational role-set enumerations ({@link readTokenRoleHolders}). `2.0.0` is + * `CrossChainToken`; `1.5.1` / `1.6.2` are `FactoryBurnMintERC20`. Mirrors + * `token-pool/contracts.ts`. * * @packageDocumentation */ -import { Interface } from 'ethers' +import { Interface, getAddress, isError } from 'ethers' +import type { TypedContract } from 'ethers-abitype' -import { CCTContractVersionUnsupportedError } from '../../errors.ts' +import type { EVMChain } from '../../../evm/index.ts' +import { CCTContractTypeInvalidError, 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' +import { getTypedContract } from '../query.ts' /** * Known token versions, low to high. `2.0.0` is `CrossChainToken`; `1.5.1` / `1.6.2` @@ -67,3 +73,116 @@ export function getTokenArtifact(version: TokenVersion): DeployArtifact { if (!artifact) throw new CCTContractVersionUnsupportedError('token', version) return artifact } + +/** + * The interface every BurnMintERC677 role/mint write encodes through. + * + * Pinned to v1.5.1: the role functions, `mint`, and the role reads are identical at v1.6.2 and + * on `HyperLiquidCompatibleERC20 1.6.2`, so there is nothing to dispatch on. v2.0.0's + * `CrossChainToken` is a different contract, ruled out by {@link readTokenRole}. + */ +export function getErc20Token(): Interface { + return TOKEN_INTERFACES[TokenVersion.V1_5_1] +} + +/** + * True for the two failure shapes a call to a function a contract does not declare produces: + * `CALL_EXCEPTION` (revert) and `BAD_DATA` (node answers `0x`). Deliberately narrow — a transport + * error or rate limit must not be read as "this contract lacks the function". + */ +function isMissingFunction(err: unknown): boolean { + return isError(err, 'CALL_EXCEPTION') || isError(err, 'BAD_DATA') +} + +/** The two role predicates, declared identically by every BurnMintERC677 token. */ +type TokenRoleReader = Pick< + TypedContract, + 'isMinter' | 'isBurner' +> + +/** + * Reads whether `account` holds one of a BurnMintERC677 token's roles, in a single `eth_call`. + * + * Doubles as the family check every role/mint write needs: only the BurnMintERC677 family + * declares these predicates, so a v2.0.0 `CrossChainToken`, a token pool, or an EOA fails here + * before an op can hand back calldata aimed at code that cannot run it. No `version` parameter — + * both predicates are identical at v1.5.1 and v1.6.2 (see {@link getErc20Token}). + * @param chain - Chain to read from. + * @param tokenAddress - Token contract to read from. + * @param read - Which role predicate to call. + * @param account - Address to test. + * @returns Whether `account` currently holds that role. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` does not declare `read` — it is + * not a BurnMintERC677 token + */ +export async function readTokenRole( + chain: EVMChain, + tokenAddress: string, + read: 'isMinter' | 'isBurner', + account: string, +): Promise { + const token: TokenRoleReader = getTypedContract( + chain, + tokenAddress, + FACTORY_BURN_MINT_ERC20_V1_5_1_ABI, + ) + try { + return await token[read](account) + } catch (err) { + if (!isMissingFunction(err)) throw err + throw new CCTContractTypeInvalidError( + tokenAddress, + 'BurnMintERC677 token (FactoryBurnMintERC20 v1.5.1 / v1.6.2)', + // the type is genuinely unknown: the contract answered nothing + 'unknown', + `it does not declare ${read}(address) — a v2.0.0 CrossChainToken gates mint/burn through AccessControl instead, and support for it ships separately`, + { cause: err instanceof Error ? err : undefined }, + ) + } +} + +/** The two role-set getters, declared identically by every BurnMintERC677 token. */ +type TokenRoleHolderReader = Pick< + TypedContract, + 'getMinters' | 'getBurners' +> + +/** + * Reads the full set of accounts holding one of a BurnMintERC677 token's roles, in a single + * `eth_call`. + * + * Informational, for audit and UX; checking one address is {@link readTokenRole}, not this set + * plus a client-side scan. Same family check and version reasoning as that read: only this family + * enumerates its role members, and both getters are identical at v1.5.1 and v1.6.2. + * @param chain - Chain to read from. + * @param tokenAddress - Token contract to read from. + * @param read - Which role set to enumerate. + * @returns The current holders, checksummed, in the order the token returns them. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` does not declare `read` — it is + * not a BurnMintERC677 token + */ +export async function readTokenRoleHolders( + chain: EVMChain, + tokenAddress: string, + read: 'getMinters' | 'getBurners', +): Promise { + const token: TokenRoleHolderReader = getTypedContract( + chain, + tokenAddress, + FACTORY_BURN_MINT_ERC20_V1_5_1_ABI, + ) + try { + // the abitype handle types an `address[]` return as `(string | Addressable)[]` + return (await token[read]()).map((holder) => getAddress(holder as string)) + } catch (err) { + if (!isMissingFunction(err)) throw err + throw new CCTContractTypeInvalidError( + tokenAddress, + 'BurnMintERC677 token (FactoryBurnMintERC20 v1.5.1 / v1.6.2)', + // the type is genuinely unknown: the contract answered nothing + 'unknown', + `it does not declare ${read}() — a v2.0.0 CrossChainToken gates mint/burn through AccessControl, which does not enumerate role members`, + { cause: err instanceof Error ? err : undefined }, + ) + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/get-burners.test.ts b/ccip-sdk/src/cct/evm/token/operations/get-burners.test.ts new file mode 100644 index 000000000..26e0d9d05 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/get-burners.test.ts @@ -0,0 +1,94 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../../../errors.ts' +import { GetBurners } from './get-burners.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const HOLDER_A = '0x' + '22'.repeat(20) +const HOLDER_B = '0x' + '33'.repeat(20) + +/** Read results from a fresh Interface, never the SDK's cached one. */ +const FRESH = new Interface(['function getBurners() view returns (address[])']) + +/** The `eth_call`s the op makes, in order, as decoded function names. */ +type Seen = { calls: string[] } +const newSeen = (): Seen => ({ calls: [] }) + +/** EVMChain stub answering `getBurners()` with `holders`. */ +function stubChain({ + holders = [HOLDER_A, HOLDER_B] as string[], + callError, + seen = newSeen(), +}: { + holders?: string[] + /** Fails every `eth_call`, standing in for a contract that is not a BurnMintERC677 token. */ + callError?: Error + seen?: Seen +} = {}): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + if (callError) return Promise.reject(callError) + const fn = FRESH.getFunction(data.slice(0, 10))!.name + seen.calls.push(fn) + return Promise.resolve(FRESH.encodeFunctionResult(fn, [holders])) + }, + }, + } as unknown as EVMChain +} + +const op = new GetBurners() + +describe('GetBurners (cct/evm)', () => { + it('lists the holders in one call', async () => { + const seen = newSeen() + const holders = await op.query(stubChain({ seen }), { tokenAddress: TOKEN }) + + assert.deepEqual(holders, [HOLDER_A, HOLDER_B]) + assert.deepEqual(seen.calls, ['getBurners']) + }) + + it('returns an empty list when no account holds the role', async () => { + assert.deepEqual(await op.query(stubChain({ holders: [] }), { tokenAddress: TOKEN }), []) + }) + + it('checksums the addresses the token returns', async () => { + const chain = stubChain({ holders: [HOLDER_A.toLowerCase()] }) + assert.deepEqual(await op.query(chain, { tokenAddress: TOKEN }), [HOLDER_A]) + }) + + for (const value of ['not-an-address', ZeroAddress]) { + it(`rejects tokenAddress = ${value} before any RPC`, async () => { + const seen = newSeen() + await assert.rejects( + () => op.query(stubChain({ seen }), { tokenAddress: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'getBurners' && + err.context.param === 'tokenAddress', + ) + assert.deepEqual(seen.calls, []) + }) + } + + it('rejects a contract that does not declare getBurners()', async () => { + // a v2.0.0 CrossChainToken does not enumerate its AccessControl role members + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => op.query(stubChain({ callError: revert }), { tokenAddress: TOKEN }), + (err: unknown) => err instanceof CCTContractTypeInvalidError && err.context.address === TOKEN, + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/get-burners.ts b/ccip-sdk/src/cct/evm/token/operations/get-burners.ts new file mode 100644 index 000000000..9fc624deb --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/get-burners.ts @@ -0,0 +1,44 @@ +/** + * getBurners — lists every account holding a BurnMintERC677 token's burn role. Informational + * (audit / UX): a *check* of one address is `isBurner`, not a scan of this set. + * + * @packageDocumentation + */ + +import type { EVMChain } from '../../../../evm/index.ts' +import { EVMQuery } from '../../query.ts' +import { validateNonZeroAddress } from '../../validate.ts' +import { readTokenRoleHolders } from '../contracts.ts' + +/** Parameters for {@link GetBurners}. */ +export type GetBurnersParams = { + /** BurnMintERC677 token (v1.5.1 / v1.6.2) to read. */ + tokenAddress: string +} + +/** Result of {@link GetBurners}: the burn-role holders, checksummed, in the token's own order. */ +export type GetBurnersResult = string[] + +/** Lists the accounts holding a BurnMintERC677 token's burn role, via `getBurners()`. */ +export class GetBurners extends EVMQuery { + readonly name = 'getBurners' + + /** + * Validates the token address; nothing to convert for {@link read}. + * @throws {@link CCTParamsInvalidError} if `tokenAddress` is not a valid, non-zero address + */ + protected prepare(params: GetBurnersParams): GetBurnersParams { + validateNonZeroAddress(this.name, 'tokenAddress', params.tokenAddress) + return params + } + + /** + * Enumerates the role set in a single `eth_call`. + * @remarks No version resolution: `getBurners()` is identical at v1.5.1 and v1.6.2, and a + * contract that does not declare it is reported by {@link readTokenRoleHolders}. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + */ + protected read(chain: EVMChain, { tokenAddress }: GetBurnersParams): Promise { + return readTokenRoleHolders(chain, tokenAddress, 'getBurners') + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/get-minters.test.ts b/ccip-sdk/src/cct/evm/token/operations/get-minters.test.ts new file mode 100644 index 000000000..eb8710d2f --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/get-minters.test.ts @@ -0,0 +1,94 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../../../errors.ts' +import { GetMinters } from './get-minters.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const HOLDER_A = '0x' + '22'.repeat(20) +const HOLDER_B = '0x' + '33'.repeat(20) + +/** Read results from a fresh Interface, never the SDK's cached one. */ +const FRESH = new Interface(['function getMinters() view returns (address[])']) + +/** The `eth_call`s the op makes, in order, as decoded function names. */ +type Seen = { calls: string[] } +const newSeen = (): Seen => ({ calls: [] }) + +/** EVMChain stub answering `getMinters()` with `holders`. */ +function stubChain({ + holders = [HOLDER_A, HOLDER_B] as string[], + callError, + seen = newSeen(), +}: { + holders?: string[] + /** Fails every `eth_call`, standing in for a contract that is not a BurnMintERC677 token. */ + callError?: Error + seen?: Seen +} = {}): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + if (callError) return Promise.reject(callError) + const fn = FRESH.getFunction(data.slice(0, 10))!.name + seen.calls.push(fn) + return Promise.resolve(FRESH.encodeFunctionResult(fn, [holders])) + }, + }, + } as unknown as EVMChain +} + +const op = new GetMinters() + +describe('GetMinters (cct/evm)', () => { + it('lists the holders in one call', async () => { + const seen = newSeen() + const holders = await op.query(stubChain({ seen }), { tokenAddress: TOKEN }) + + assert.deepEqual(holders, [HOLDER_A, HOLDER_B]) + assert.deepEqual(seen.calls, ['getMinters']) + }) + + it('returns an empty list when no account holds the role', async () => { + assert.deepEqual(await op.query(stubChain({ holders: [] }), { tokenAddress: TOKEN }), []) + }) + + it('checksums the addresses the token returns', async () => { + const chain = stubChain({ holders: [HOLDER_A.toLowerCase()] }) + assert.deepEqual(await op.query(chain, { tokenAddress: TOKEN }), [HOLDER_A]) + }) + + for (const value of ['not-an-address', ZeroAddress]) { + it(`rejects tokenAddress = ${value} before any RPC`, async () => { + const seen = newSeen() + await assert.rejects( + () => op.query(stubChain({ seen }), { tokenAddress: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'getMinters' && + err.context.param === 'tokenAddress', + ) + assert.deepEqual(seen.calls, []) + }) + } + + it('rejects a contract that does not declare getMinters()', async () => { + // a v2.0.0 CrossChainToken does not enumerate its AccessControl role members + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => op.query(stubChain({ callError: revert }), { tokenAddress: TOKEN }), + (err: unknown) => err instanceof CCTContractTypeInvalidError && err.context.address === TOKEN, + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/get-minters.ts b/ccip-sdk/src/cct/evm/token/operations/get-minters.ts new file mode 100644 index 000000000..f25f3ff2a --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/get-minters.ts @@ -0,0 +1,44 @@ +/** + * getMinters — lists every account holding a BurnMintERC677 token's mint role. Informational + * (audit / UX): a *check* of one address is `isMinter`, not a scan of this set. + * + * @packageDocumentation + */ + +import type { EVMChain } from '../../../../evm/index.ts' +import { EVMQuery } from '../../query.ts' +import { validateNonZeroAddress } from '../../validate.ts' +import { readTokenRoleHolders } from '../contracts.ts' + +/** Parameters for {@link GetMinters}. */ +export type GetMintersParams = { + /** BurnMintERC677 token (v1.5.1 / v1.6.2) to read. */ + tokenAddress: string +} + +/** Result of {@link GetMinters}: the mint-role holders, checksummed, in the token's own order. */ +export type GetMintersResult = string[] + +/** Lists the accounts holding a BurnMintERC677 token's mint role, via `getMinters()`. */ +export class GetMinters extends EVMQuery { + readonly name = 'getMinters' + + /** + * Validates the token address; nothing to convert for {@link read}. + * @throws {@link CCTParamsInvalidError} if `tokenAddress` is not a valid, non-zero address + */ + protected prepare(params: GetMintersParams): GetMintersParams { + validateNonZeroAddress(this.name, 'tokenAddress', params.tokenAddress) + return params + } + + /** + * Enumerates the role set in a single `eth_call`. + * @remarks No version resolution: `getMinters()` is identical at v1.5.1 and v1.6.2, and a + * contract that does not declare it is reported by {@link readTokenRoleHolders}. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + */ + protected read(chain: EVMChain, { tokenAddress }: GetMintersParams): Promise { + return readTokenRoleHolders(chain, tokenAddress, 'getMinters') + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/is-burner.test.ts b/ccip-sdk/src/cct/evm/token/operations/is-burner.test.ts new file mode 100644 index 000000000..569cefc1f --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/is-burner.test.ts @@ -0,0 +1,94 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../../../errors.ts' +import { IsBurner } from './is-burner.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const ACCOUNT = '0x' + '22'.repeat(20) + +/** Read results from a fresh Interface, never the SDK's cached one. */ +const FRESH = new Interface(['function isBurner(address) view returns (bool)']) + +/** The `eth_call`s the op makes, in order, as decoded function names. */ +type Seen = { calls: string[]; args: string[] } +const newSeen = (): Seen => ({ calls: [], args: [] }) + +/** EVMChain stub answering `isBurner(address)` with `holds`. */ +function stubChain({ + holds = true, + callError, + seen = newSeen(), +}: { + holds?: boolean + /** Fails every `eth_call`, standing in for a contract that is not a BurnMintERC677 token. */ + callError?: Error + seen?: Seen +} = {}): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + if (callError) return Promise.reject(callError) + const fn = FRESH.getFunction(data.slice(0, 10))! + seen.calls.push(fn.name) + seen.args.push(FRESH.decodeFunctionData(fn, data)[0] as string) + return Promise.resolve(FRESH.encodeFunctionResult(fn, [holds])) + }, + }, + } as unknown as EVMChain +} + +const op = new IsBurner() + +describe('IsBurner (cct/evm)', () => { + it('answers true for a role holder in one call', async () => { + const seen = newSeen() + const holds = await op.query(stubChain({ seen }), { tokenAddress: TOKEN, account: ACCOUNT }) + + assert.equal(holds, true) + assert.deepEqual(seen.calls, ['isBurner']) + assert.deepEqual(seen.args, [ACCOUNT]) + }) + + it('answers false for an account without the role', async () => { + const chain = stubChain({ holds: false }) + assert.equal(await op.query(chain, { tokenAddress: TOKEN, account: ACCOUNT }), false) + }) + + for (const param of ['tokenAddress', 'account'] as const) { + for (const value of ['not-an-address', ZeroAddress]) { + it(`rejects ${param} = ${value} before any RPC`, async () => { + const seen = newSeen() + const params = { tokenAddress: TOKEN, account: ACCOUNT, [param]: value } + await assert.rejects( + () => op.query(stubChain({ seen }), params), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'isBurner' && + err.context.param === param, + ) + assert.deepEqual(seen.calls, []) + }) + } + } + + it('rejects a contract that does not declare isBurner(address)', async () => { + // a v2.0.0 CrossChainToken gates burning through AccessControl instead + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => op.query(stubChain({ callError: revert }), { tokenAddress: TOKEN, account: ACCOUNT }), + (err: unknown) => err instanceof CCTContractTypeInvalidError && err.context.address === TOKEN, + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/is-burner.ts b/ccip-sdk/src/cct/evm/token/operations/is-burner.ts new file mode 100644 index 000000000..6dc6029b4 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/is-burner.ts @@ -0,0 +1,49 @@ +/** + * isBurner — whether one account holds a BurnMintERC677 token's burn role. The check a caller + * wants before a burn; enumerating the whole set is `getBurners`. + * + * @packageDocumentation + */ + +import type { EVMChain } from '../../../../evm/index.ts' +import { EVMQuery } from '../../query.ts' +import { validateNonZeroAddress } from '../../validate.ts' +import { readTokenRole } from '../contracts.ts' + +/** Parameters for {@link IsBurner}. */ +export type IsBurnerParams = { + /** BurnMintERC677 token (v1.5.1 / v1.6.2) to read. */ + tokenAddress: string + /** Account to test for the burn role. */ + account: string +} + +/** Result of {@link IsBurner}: whether `account` currently holds the token's burn role. */ +export type IsBurnerResult = boolean + +/** Reads whether an account holds a BurnMintERC677 token's burn role, via `isBurner(address)`. */ +export class IsBurner extends EVMQuery { + readonly name = 'isBurner' + + /** + * Validates both addresses; nothing to convert for {@link read}. + * @remarks `account` is rejected as the zero address for the same reason as in + * {@link IsMinter.prepare}: the call could only ever answer `false`. + * @throws {@link CCTParamsInvalidError} if either address is not a valid, non-zero address + */ + protected prepare(params: IsBurnerParams): IsBurnerParams { + validateNonZeroAddress(this.name, 'tokenAddress', params.tokenAddress) + validateNonZeroAddress(this.name, 'account', params.account) + return params + } + + /** + * Reads the role predicate in a single `eth_call`. + * @remarks No version resolution: `isBurner(address)` is identical at v1.5.1 and v1.6.2, and a + * contract that does not declare it is reported by {@link readTokenRole}. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + */ + protected read(chain: EVMChain, { tokenAddress, account }: IsBurnerParams): Promise { + return readTokenRole(chain, tokenAddress, 'isBurner', account) + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/is-minter.test.ts b/ccip-sdk/src/cct/evm/token/operations/is-minter.test.ts new file mode 100644 index 000000000..1172b8f11 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/is-minter.test.ts @@ -0,0 +1,94 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import type { EVMChain } from '../../../../evm/index.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../../../errors.ts' +import { IsMinter } from './is-minter.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const ACCOUNT = '0x' + '22'.repeat(20) + +/** Read results from a fresh Interface, never the SDK's cached one. */ +const FRESH = new Interface(['function isMinter(address) view returns (bool)']) + +/** The `eth_call`s the op makes, in order, as decoded function names. */ +type Seen = { calls: string[]; args: string[] } +const newSeen = (): Seen => ({ calls: [], args: [] }) + +/** EVMChain stub answering `isMinter(address)` with `holds`. */ +function stubChain({ + holds = true, + callError, + seen = newSeen(), +}: { + holds?: boolean + /** Fails every `eth_call`, standing in for a contract that is not a BurnMintERC677 token. */ + callError?: Error + seen?: Seen +} = {}): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + if (callError) return Promise.reject(callError) + const fn = FRESH.getFunction(data.slice(0, 10))! + seen.calls.push(fn.name) + seen.args.push(FRESH.decodeFunctionData(fn, data)[0] as string) + return Promise.resolve(FRESH.encodeFunctionResult(fn, [holds])) + }, + }, + } as unknown as EVMChain +} + +const op = new IsMinter() + +describe('IsMinter (cct/evm)', () => { + it('answers true for a role holder in one call', async () => { + const seen = newSeen() + const holds = await op.query(stubChain({ seen }), { tokenAddress: TOKEN, account: ACCOUNT }) + + assert.equal(holds, true) + assert.deepEqual(seen.calls, ['isMinter']) + assert.deepEqual(seen.args, [ACCOUNT]) + }) + + it('answers false for an account without the role', async () => { + const chain = stubChain({ holds: false }) + assert.equal(await op.query(chain, { tokenAddress: TOKEN, account: ACCOUNT }), false) + }) + + for (const param of ['tokenAddress', 'account'] as const) { + for (const value of ['not-an-address', ZeroAddress]) { + it(`rejects ${param} = ${value} before any RPC`, async () => { + const seen = newSeen() + const params = { tokenAddress: TOKEN, account: ACCOUNT, [param]: value } + await assert.rejects( + () => op.query(stubChain({ seen }), params), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'isMinter' && + err.context.param === param, + ) + assert.deepEqual(seen.calls, []) + }) + } + } + + it('rejects a contract that does not declare isMinter(address)', async () => { + // a v2.0.0 CrossChainToken gates minting through AccessControl instead + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => op.query(stubChain({ callError: revert }), { tokenAddress: TOKEN, account: ACCOUNT }), + (err: unknown) => err instanceof CCTContractTypeInvalidError && err.context.address === TOKEN, + ) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/is-minter.ts b/ccip-sdk/src/cct/evm/token/operations/is-minter.ts new file mode 100644 index 000000000..09081ec28 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/is-minter.ts @@ -0,0 +1,50 @@ +/** + * isMinter — whether one account holds a BurnMintERC677 token's mint role. The check a caller + * wants before a `mint`; enumerating the whole set is `getMinters`. + * + * @packageDocumentation + */ + +import type { EVMChain } from '../../../../evm/index.ts' +import { EVMQuery } from '../../query.ts' +import { validateNonZeroAddress } from '../../validate.ts' +import { readTokenRole } from '../contracts.ts' + +/** Parameters for {@link IsMinter}. */ +export type IsMinterParams = { + /** BurnMintERC677 token (v1.5.1 / v1.6.2) to read. */ + tokenAddress: string + /** Account to test for the mint role. */ + account: string +} + +/** Result of {@link IsMinter}: whether `account` currently holds the token's mint role. */ +export type IsMinterResult = boolean + +/** Reads whether an account holds a BurnMintERC677 token's mint role, via `isMinter(address)`. */ +export class IsMinter extends EVMQuery { + readonly name = 'isMinter' + + /** + * Validates both addresses; nothing to convert for {@link read}. + * @remarks `account` is rejected as the zero address: the token can never grant a role to it, + * so the call could only ever answer `false` — a caller passing it has a bug worth surfacing + * rather than an answer worth an RPC. + * @throws {@link CCTParamsInvalidError} if either address is not a valid, non-zero address + */ + protected prepare(params: IsMinterParams): IsMinterParams { + validateNonZeroAddress(this.name, 'tokenAddress', params.tokenAddress) + validateNonZeroAddress(this.name, 'account', params.account) + return params + } + + /** + * Reads the role predicate in a single `eth_call`. + * @remarks No version resolution: `isMinter(address)` is identical at v1.5.1 and v1.6.2, and a + * contract that does not declare it is reported by {@link readTokenRole}. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + */ + protected read(chain: EVMChain, { tokenAddress, account }: IsMinterParams): Promise { + return readTokenRole(chain, tokenAddress, 'isMinter', account) + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/mint.test.ts b/ccip-sdk/src/cct/evm/token/operations/mint.test.ts new file mode 100644 index 000000000..e0ee3dabc --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/mint.test.ts @@ -0,0 +1,277 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../../../errors.ts' +import { type MintParams, Mint } from './mint.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const MINTER = '0x' + '22'.repeat(20) +const RECIPIENT = '0x' + '33'.repeat(20) +const NOT_A_MINTER = '0x' + '44'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) +const AMOUNT = 1_000000000000000000n + +/** Calldata built from a fresh Interface — never the SDK's cached one, or this proves nothing. */ +const FRESH = new Interface([ + 'function mint(address account, uint256 amount)', + 'function isMinter(address minter) view returns (bool)', +]) +const expectedData = (account = RECIPIENT, amount = AMOUNT) => + FRESH.encodeFunctionData('mint', [account, amount]) + +/** The `eth_call`s the op makes, in order, as decoded function names. */ +type Seen = { calls: string[] } +const newSeen = (): Seen => ({ calls: [] }) + +/** EVMChain stub answering `isMinter` off a fresh Interface. */ +function stubChain({ + isMinter = true, + callError, + seen = newSeen(), +}: { + isMinter?: boolean + /** Fails every `eth_call`, standing in for a contract that is not a BurnMintERC677 token. */ + callError?: Error + seen?: Seen +} = {}): EVMChain { + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + if (callError) return Promise.reject(callError) + const fn = FRESH.getFunction(data.slice(0, 10))!.name + seen.calls.push(fn) + return Promise.resolve(FRESH.encodeFunctionResult(fn, [isMinter])) + }, + }, + nextNonce: () => Promise.resolve(0), + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(waitError?: Error, address = MINTER) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + 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 Mint() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { + tokenAddress: TOKEN, + account: RECIPIENT, + amount: AMOUNT, + sender: MINTER, + ...overrides, + }) +} + +describe('Mint (cct/evm)', () => { + describe('generate', () => { + it('encodes mint(address,uint256) to the token', 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, TOKEN) + assert.equal(tx.from, MINTER) + assert.equal(tx.data, expectedData()) + }) + + it('encodes the full uint256 range', async () => { + const amount = 2n ** 256n - 1n + const unsigned = await generate(stubChain(), { amount }) + assert.equal(unsigned.transactions[0]!.data, expectedData(RECIPIENT, amount)) + }) + + it('accepts a zero amount, which the token mines as a Transfer of nothing', async () => { + const unsigned = await generate(stubChain(), { amount: 0n }) + assert.equal(unsigned.transactions[0]!.data, expectedData(RECIPIENT, 0n)) + }) + + it('omits from when sender is not supplied, but still probes the token', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen }), { sender: undefined }) + + assert.equal(unsigned.transactions[0]!.from, undefined) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + // the read doubles as the family check, so it runs with no sender to compare + assert.deepEqual(seen.calls, ['isMinter']) + }) + + it('pre-flights with exactly one isMinter read', async () => { + const seen = newSeen() + await generate(stubChain({ seen })) + assert.deepEqual(seen.calls, ['isMinter']) + }) + }) + + describe('validation', () => { + for (const [param, value] of [ + ['tokenAddress', 'not-an-address'], + // a tx to `0x0` hits no code, so it mines as a successful no-op instead of reverting + ['tokenAddress', ZeroAddress], + ['account', 'not-an-address'], + // the token's own _mint reverts on a zero recipient + ['account', ZeroAddress], + ['amount', 1 as never], + ['amount', -1n], + ['amount', 2n ** 256n], + ['sender', 'not-an-address'], + ] as const) { + it(`rejects ${param} = ${String(value)} before any RPC`, async () => { + const seen = newSeen() + await assert.rejects( + () => generate(stubChain({ seen }), { [param]: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'mint' && + err.context.param === param, + ) + assert.deepEqual(seen.calls, []) + }) + } + }) + + describe('family check', () => { + it('rejects a contract that is not a BurnMintERC677 token', async () => { + // a v2.0.0 CrossChainToken, a token pool, and an EOA all fail the isMinter read + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => generate(stubChain({ callError: revert })), + (err: unknown) => + err instanceof CCTContractTypeInvalidError && err.context.address === TOKEN, + ) + }) + + it('rejects an unrelated address even with no sender to check', async () => { + // the case a role gate alone would miss: a mint tx to codeless address mines as a no-op + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => generate(stubChain({ callError: revert }), { sender: undefined }), + (err: unknown) => err instanceof CCTContractTypeInvalidError, + ) + }) + }) + + describe('role gate', () => { + it('rejects a sender that does not hold the mint role', async () => { + await assert.rejects( + () => generate(stubChain({ isMinter: false }), { sender: NOT_A_MINTER }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'mint' && + err.context.param === 'sender' && + /must hold the mint role/.test(String(err.context.reason)), + ) + }) + + it('does not gate on the owner — a minter that is not the owner still builds', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen })) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + assert.ok(!seen.calls.includes('owner'), 'mint is onlyMinter, not onlyOwner') + }) + }) + + describe('execute', () => { + it('submits as the minting wallet and returns the tx hash', async () => { + const { hash } = await op.execute(stubChain(), { + tokenAddress: TOKEN, + account: RECIPIENT, + amount: AMOUNT, + wallet: fakeSigner(), + }) + assert.equal(hash, HASH) + }) + + it('rejects a sender that is not the signing wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + account: RECIPIENT, + amount: AMOUNT, + sender: NOT_A_MINTER, + wallet: fakeSigner(), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a wallet that does not hold the mint role', async () => { + await assert.rejects( + () => + op.execute(stubChain({ isMinter: false }), { + tokenAddress: TOKEN, + account: RECIPIENT, + amount: AMOUNT, + wallet: fakeSigner(undefined, NOT_A_MINTER), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('surfaces an on-chain revert — e.g. a mint past maxSupply, which is not pre-flighted', async () => { + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'sendTransaction', + data: '0x', + reason: 'MaxSupplyExceeded', + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + account: RECIPIENT, + amount: AMOUNT, + wallet: fakeSigner(revert), + }), + (err: unknown) => err instanceof CCIPExecTxRevertedError, + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + account: RECIPIENT, + amount: AMOUNT, + wallet: {}, + }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/mint.ts b/ccip-sdk/src/cct/evm/token/operations/mint.ts new file mode 100644 index 000000000..11c3c6375 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/mint.ts @@ -0,0 +1,95 @@ +/** + * mint — mints new supply of a BurnMintERC677 token to an account. A role-gated manual mint, for + * seeding liquidity or topping up test supply; the bridge path mints through the pool instead. + * + * @packageDocumentation + */ + +import { ZeroAddress } 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 { validateNonZeroAddress, validateUint256 } from '../../validate.ts' +import { getErc20Token, readTokenRole } from '../contracts.ts' + +/** Parameters for {@link Mint}. */ +export type MintParams = { + /** BurnMintERC677 token (v1.5.1 / v1.6.2) to mint. */ + tokenAddress: string + /** Account credited with the newly minted supply. */ + account: string + /** Amount to mint, in the token's smallest unit (`uint256`). */ + amount: bigint + /** Address holding the token's mint role; sets `tx.from` for offline / multisig signing. */ + sender?: string +} + +/** Mints new supply of a BurnMintERC677 token to an account. Gated on the token's mint role. */ +export class Mint extends EVMOperation { + readonly name = 'mint' + + /** + * Validates the token, recipient and amount before any RPC. + * @remarks `account` is rejected as the zero address, which the token's own `_mint` reverts on + * (`ERC20: mint to the zero address`). A zero `amount` is *not* rejected: it mines successfully + * as a `Transfer` of nothing, and accepting it keeps this op's contract the token's own. + * @throws {@link CCTParamsInvalidError} if any param is invalid + */ + protected override validate({ tokenAddress, account, amount }: MintParams): void { + validateNonZeroAddress(this.name, 'tokenAddress', tokenAddress) + validateNonZeroAddress(this.name, 'account', account) + validateUint256(this.name, 'amount', amount) + } + + /** + * Confirms `sender` holds the token's mint role before encoding. + * + * Gated on `isMinter(sender)`, not `owner()`: `mint` is `onlyMinter`, and the owner is only the + * role admin, who need not hold the role. The read runs even with no `sender` to compare + * (against the zero address, answer discarded) because it is also the family check + * ({@link readTokenRole}) — a `mint` built for an address with no code would otherwise mine + * successfully and mint nothing. It runs here rather than in {@link execute} so the offline / + * multisig path is gated too. A mint past a capped token's `maxSupply` is not pre-flighted. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * @throws {@link CCTParamsInvalidError} if `sender` is given and does not hold the mint role + */ + protected async buildUnsigned( + chain: EVMChain, + { tokenAddress, account, amount, sender }: MintParams, + ): Promise { + const isMinter = await readTokenRole(chain, tokenAddress, 'isMinter', sender ?? ZeroAddress) + if (sender !== undefined && !isMinter) + throw new CCTParamsInvalidError( + this.name, + 'sender', + `must hold the mint role on ${tokenAddress} — grant it with grantMintRole (or grantMintAndBurnRoles) as the token owner`, + ) + + return callTx(tokenAddress, getErc20Token().encodeFunctionData('mint', [account, amount])) + } + + /** + * Signs and submits as a minter, defaulting `sender` to the signing wallet — the only address + * that can satisfy {@link buildUnsigned}'s role check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} for why a divergent `sender` is rejected. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * @throws {@link CCTParamsInvalidError} if `sender` is given and is not the wallet's address, if + * the wallet does not hold the mint role, or if any other param is invalid (see + * {@link buildUnsigned}) + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain — e.g. the mint would + * exceed the token's `maxSupply`, which is not pre-flighted + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} From 2cd49b20ed443cbba4f90ca99b7327022aa08984 Mon Sep 17 00:00:00 2001 From: Mervin Date: Thu, 10 Sep 2026 15:02:53 +0800 Subject: [PATCH 86/87] fix(cct-sdk): Harden Solana operation validation (#414) * refactor(cct-sdk): reuse Solana execution preparation * fix(cct-sdk): bound Solana preMint to u64 * fix(cct-sdk): reject partial canonical ALT blocks * fix(cct-sdk): reject duplicate Solana pool configuration * chore: update image-size lockfile * style(ccip-sdk): format gas fallback * fix: address comments --- .../operations/accept-admin.ts | 10 +-- .../operations/append-to-lookup-table.test.ts | 27 +++++++ .../operations/append-to-lookup-table.ts | 24 ++++-- .../operations/apply-chain-updates.test.ts | 24 ++++++ .../operations/apply-chain-updates.ts | 21 ++++- .../operations/configure-allowlist.test.ts | 2 +- .../operations/configure-allowlist.ts | 22 ++---- .../operations/deploy-token-pool.test.ts | 9 +++ .../operations/deploy-token-pool.ts | 21 +++-- .../edit-chain-remote-config.test.ts | 1 + .../operations/edit-chain-remote-config.ts | 7 ++ .../operations/remove-from-allowlist.test.ts | 2 +- .../operations/remove-from-allowlist.ts | 22 ++---- .../token/operations/deploy-token.test.ts | 2 + .../solana/token/operations/deploy-token.ts | 16 ++-- ccip-sdk/src/cct/solana/validate.test.ts | 48 ++++++++++++ ccip-sdk/src/cct/solana/validate.ts | 76 ++++++++++++++++++- ccip-sdk/src/gas.ts | 3 +- package-lock.json | 15 +++- 19 files changed, 290 insertions(+), 62 deletions(-) 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 index bf2828f77..5b2762aff 100644 --- 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 @@ -1,9 +1,8 @@ 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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' import { @@ -117,12 +116,7 @@ export class AcceptAdmin extends SolanaOperation< 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) + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) if (params.authority !== undefined) { validateAuthorityMatchesWallet( 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 index 7487e9fca..68da8b831 100644 --- 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 @@ -183,6 +183,24 @@ describe('AppendToLookupTable (cct/solana)', () => { ) }) + it('rejects a partial canonical CCIP address block', async () => { + const ccipAddresses = await deriveCcipLookupTableAddresses(stubChain(), { + 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[0]!] }), + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'lookupTableAddress', + ) + }) + it('defaults omitted additional addresses to an empty list', async () => { const unsigned = await generate({ additionalAddresses: undefined, @@ -271,6 +289,15 @@ describe('AppendToLookupTable (cct/solana)', () => { assert.equal(getLookupTableCalls, 0) }) + it('rejects duplicate additional addresses', async () => { + const address = Keypair.generate().publicKey.toBase58() + await assert.rejects( + () => generate({ additionalAddresses: [address, address] }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'additionalAddresses', + ) + }) + it('requires at least one address source', async () => { await assert.rejects( () => generate({ additionalAddresses: [] }), 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 index 9766a389c..a1e35fbf6 100644 --- 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 @@ -157,6 +157,9 @@ export class AppendToLookupTable extends SolanaOperation< ) } + const existingAddresses = new Set( + lookupTable.value.state.addresses.map((address) => address.toBase58()), + ) const addresses = [...opts.additionalAddresses] if (opts.tokenMint && poolProgram) { @@ -166,21 +169,30 @@ export class AppendToLookupTable extends SolanaOperation< tokenMint, poolProgram, }) - const existingAddresses = new Set( - lookupTable.value.state.addresses.map((address) => address.toBase58()), - ) - - if (ccipAddresses.every((address) => existingAddresses.has(address.toBase58()))) { + if (ccipAddresses.some((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', + 'lookup table already contains canonical CCIP addresses; only append additionalAddresses or use an empty ALT', ) } addresses.unshift(...ccipAddresses) } + const appendedAddresses = new Set() + for (const address of addresses) { + const value = address.toBase58() + if (existingAddresses.has(value) || appendedAddresses.has(value)) { + throw new CCTParamsInvalidError( + this.name, + 'additionalAddresses', + 'must not contain addresses already in the ALT or duplicate addresses', + ) + } + appendedAddresses.add(value) + } + const totalAddressesAfterAppend = lookupTable.value.state.addresses.length + addresses.length if (totalAddressesAfterAppend > MAX_ALT_ADDRESSES) { throw new CCTParamsInvalidError( 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 index 15fadf121..0ecc1e04f 100644 --- 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 @@ -281,6 +281,30 @@ describe('ApplyChainUpdates (cct/solana)', () => { [{ chainsToAdd: null }, 'chainsToAdd'], [{ chainsToAdd: [], remoteChainSelectorsToRemove: [] }, 'chainsToAdd'], [{ chainsToAdd: [null] }, 'chainsToAdd[0]'], + [{ remoteChainSelectorsToRemove: [SELECTOR, SELECTOR] }, 'remoteChainSelectorsToRemove[1]'], + [ + { + chainsToAdd: [ + { + remoteChainSelector: SELECTOR, + remoteTokenAddress: '0x1234567890abcdef1234567890abcdef12345678', + remotePoolAddresses: [], + remoteTokenDecimals: 18, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: false }, + }, + { + remoteChainSelector: SELECTOR, + remoteTokenAddress: '0xaabbccddeeff00112233445566778899aabbccdd', + remotePoolAddresses: [], + remoteTokenDecimals: 18, + inboundRateLimiterConfig: { enabled: false }, + outboundRateLimiterConfig: { enabled: false }, + }, + ], + }, + 'chainsToAdd[1]', + ], [ { chainsToAdd: [ 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 index 258ff1732..fd8eafd14 100644 --- 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 @@ -24,6 +24,7 @@ import { parsePublicKey, resolvePoolProgram, validateAuthorityMatchesWallet, + validateUniqueChainSelectors, } from '../../validate.ts' import { DeleteChainRemoteConfig } from './delete-chain-remote-config.ts' import { EditChainRemoteConfig } from './edit-chain-remote-config.ts' @@ -195,7 +196,8 @@ export type ExecuteApplyChainUpdatesResult = { hashes: string[]; chainSelectors: * Applies the EVM `applyChainUpdates` equivalent as Solana instructions. * * @remarks - * This preserves EVM ordering: all removals run first, then each added chain is initialized, + * Chain selectors to add and remove must not contain duplicates. 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 @@ -227,7 +229,22 @@ export class ApplyChainUpdates extends SolanaOperation< 'at least one of chainsToAdd or remoteChainSelectorsToRemove must be non-empty', ) } - validateRemotePoolAddresses(this.name, params.chainsToAdd) + validateUniqueChainSelectors( + this.name, + 'remoteChainSelectorsToRemove', + params.remoteChainSelectorsToRemove, + ) + const chainsToAdd: unknown[] = params.chainsToAdd + validateUniqueChainSelectors( + this.name, + 'chainsToAdd', + chainsToAdd.map((update) => + typeof update === 'object' && update !== null + ? (update as { remoteChainSelector?: unknown }).remoteChainSelector + : undefined, + ), + ) + validateRemotePoolAddresses(this.name, chainsToAdd) return { ...params, 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 index 82eb3a6c7..d344e93c7 100644 --- 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 @@ -159,7 +159,7 @@ describe('ConfigureAllowlist (cct/solana)', () => { (err: unknown) => err instanceof CCTParamsInvalidError && err.context.operation === 'configureAllowlist' && - err.context.param === 'add', + err.context.param === 'add[1]', ) }) 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 index fba26abf1..22a66a991 100644 --- a/ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.ts +++ b/ccip-sdk/src/cct/solana/token-pool/operations/configure-allowlist.ts @@ -1,9 +1,8 @@ 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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' import { @@ -21,6 +20,7 @@ import { parsePublicKey, resolvePoolProgram, validateAuthorityMatchesWallet, + validateUniquePublicKeys, } from '../../validate.ts' /** Parameters shared by Solana token pool `configureAllowlist` generation and execution. */ @@ -56,7 +56,10 @@ export type ExecuteConfigureAllowlistParams = SolanaExecuteParams 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') - } + validateUniquePublicKeys(this.name, 'add', add) const payer = parsePublicKey(this.name, 'payer', params.payer) return { @@ -125,14 +126,7 @@ export class ConfigureAllowlist extends SolanaOperation< 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) + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) if (params.authority !== undefined) { validateAuthorityMatchesWallet( 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 index 42cde3de0..ccd7080de 100644 --- 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 @@ -156,6 +156,15 @@ describe('DeployTokenPool (cct/solana)', () => { ) }) + it('rejects duplicate allowlist addresses', async () => { + const address = Keypair.generate().publicKey.toBase58() + await assert.rejects( + () => generate({ allowlist: [address, address] }), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'allowlist[1]', + ) + }) + it('rejects invalid allowlist addresses', async () => { await assert.rejects( () => generate({ allowlist: ['not-a-pubkey'] }), 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 index 23c6388af..2f735c3ee 100644 --- 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 @@ -21,7 +21,12 @@ import { } from '../../programs/token-pool.ts' import { submit } from '../../submit.ts' import { CreateTokenAccount } from '../../token/operations/create-token-account.ts' -import { parsePublicKey, validateAuthorityMatchesWallet, validatePoolType } from '../../validate.ts' +import { + parsePublicKey, + validateAuthorityMatchesWallet, + validatePoolType, + validateUniquePublicKeys, +} from '../../validate.ts' /** * Parameters for initializing a Solana token pool, optionally with an allowlist. @@ -85,7 +90,10 @@ export type ExecuteDeployTokenPoolResult = TransactionResult & { poolSignerAddress: string } -/** Initializes a Solana token pool, optionally configuring an allowlist. */ +/** + * Initializes a Solana token pool, optionally configuring an allowlist. + * @remarks The allowlist must not contain duplicate addresses. + */ export class DeployTokenPool extends SolanaOperation< DeployTokenPoolParams, GenerateDeployTokenPoolResult, @@ -106,6 +114,11 @@ export class DeployTokenPool extends SolanaOperation< throw new CCTParamsInvalidError(this.name, 'createPoolSignerATA', 'must be a boolean') } + const allowlist = (params.allowlist ?? []).map((address, i) => + parsePublicKey(this.name, `allowlist[${i}]`, address), + ) + validateUniquePublicKeys(this.name, 'allowlist', allowlist) + const payer = parsePublicKey(this.name, 'payer', params.payer) return { tokenMint: parsePublicKey(this.name, 'tokenAddress', params.tokenAddress), @@ -115,9 +128,7 @@ export class DeployTokenPool extends SolanaOperation< params.authority === undefined ? payer : parsePublicKey(this.name, 'authority', params.authority), - allowlist: (params.allowlist ?? []).map((address, i) => - parsePublicKey(this.name, `allowlist[${i}]`, address), - ), + allowlist, createPoolSignerATA: params.createPoolSignerATA ?? false, } } 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 index c48a13f89..312a69e18 100644 --- 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 @@ -140,6 +140,7 @@ describe('EditChainRemoteConfig (cct/solana)', () => { [{ remoteTokenAddress: '0x123' }, 'remoteTokenAddress'], [{ remotePoolAddresses: [''] }, 'remotePoolAddresses[0]'], [{ remotePoolAddresses: ['0x123'] }, 'remotePoolAddresses[0]'], + [{ remotePoolAddresses: ['0x1234', '0x1234'] }, 'remotePoolAddresses[1]'], [{ remotePoolAddresses: '0x12' }, 'remotePoolAddresses'], [{ remoteTokenDecimals: 256 }, 'remoteTokenDecimals'], ] as const) { 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 index fc5f46f44..406a0b83d 100644 --- 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 @@ -29,6 +29,7 @@ import { validateAuthorityMatchesWallet, validateBigInt, validateInteger, + validateUniqueHexBytes, } from '../../validate.ts' /** Parameters shared by Solana token pool remote-config editing generation and execution. */ @@ -107,6 +108,12 @@ export class EditChainRemoteConfig extends SolanaOperation< const remotePoolAddresses = params.remotePoolAddresses.map((address, i) => parseNonEmptyHexBytes(this.name, `remotePoolAddresses[${i}]`, address), ) + validateUniqueHexBytes( + this.name, + 'remotePoolAddresses', + remotePoolAddresses, + 'remote pool addresses', + ) const payer = parsePublicKey(this.name, 'payer', params.payer) return { 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 index f0a1e2415..b0c9ef8a5 100644 --- 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 @@ -153,7 +153,7 @@ describe('RemoveFromAllowlist (cct/solana)', () => { (err: unknown) => err instanceof CCTParamsInvalidError && err.context.operation === 'removeFromAllowlist' && - err.context.param === 'remove', + err.context.param === 'remove[1]', ) }) }) 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 index 42a4f2dea..bd2e01a8f 100644 --- 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 @@ -1,9 +1,8 @@ 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 type { UnsignedSolanaTx } from '../../../../solana/types.ts' import { CCTParamsInvalidError } from '../../../errors.ts' import type { TransactionResult } from '../../../operation.ts' import { @@ -21,6 +20,7 @@ import { parsePublicKey, resolvePoolProgram, validateAuthorityMatchesWallet, + validateUniquePublicKeys, } from '../../validate.ts' /** Parameters shared by Solana token pool `removeFromAllowlist` generation and execution. */ @@ -57,7 +57,10 @@ export type ExecuteRemoveFromAllowlistParams = SolanaExecuteParams 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') - } + validateUniquePublicKeys(this.name, 'remove', remove) const payer = parsePublicKey(this.name, 'payer', params.payer) return { @@ -122,14 +123,7 @@ export class RemoveFromAllowlist extends SolanaOperation< 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) + const { wallet, computeUnits, parsed } = this.prepareWalletExecution(params) if (params.authority !== undefined) { validateAuthorityMatchesWallet( 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 index 83b789d43..8a30dd918 100644 --- a/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.test.ts @@ -7,6 +7,7 @@ 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 { U64_MAX } from '../../validate.ts' import { DeployToken } from './deploy-token.ts' const BLOCKHASH = PublicKey.default.toBase58() @@ -129,6 +130,7 @@ describe('DeployToken (cct/solana)', () => { [{ freezeAuthority: 'invalid' }, 'freezeAuthority'], [{ preMint: 0n }, 'preMint'], [{ preMint: 1 }, 'preMint'], + [{ preMint: U64_MAX + 1n }, 'preMint'], [{ preMint: 1n }, 'preMintRecipient'], [{ preMint: 1n, preMintRecipient: 'invalid' }, 'preMintRecipient'], ] as const) { diff --git a/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts index ff8b8455a..b44a211cc 100644 --- a/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts +++ b/ccip-sdk/src/cct/solana/token/operations/deploy-token.ts @@ -21,7 +21,12 @@ import { } from '../../operation.ts' import { deriveMetadataAddress } from '../../programs/token.ts' import { submit } from '../../submit.ts' -import { validateOptionalPublicKey, validatePublicKey } from '../../validate.ts' +import { + U64_MAX, + validateBigInt, + validateOptionalPublicKey, + validatePublicKey, +} from '../../validate.ts' type BaseDeployTokenParams = { /** Mint decimals. Must be an integer between 0 and 255. */ @@ -32,7 +37,7 @@ type BaseDeployTokenParams = { mintAuthority?: string /** Freeze authority. Defaults to payer; set null to disable freezing. */ freezeAuthority?: string | null - /** Initial supply in base units. Requires preMintRecipient. */ + /** Initial supply in base units, between 1 and 2^64 - 1. Requires preMintRecipient. */ preMint?: bigint /** Recipient owner for the initial supply ATA. */ preMintRecipient?: string @@ -248,11 +253,8 @@ function validateBaseParams(operation: string, params: GenerateDeployTokenParams } 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) { + validateBigInt(operation, 'preMint', params.preMint, 1n, U64_MAX) } if (params.preMint !== undefined && !params.preMintRecipient) { throw new CCTParamsInvalidError( diff --git a/ccip-sdk/src/cct/solana/validate.test.ts b/ccip-sdk/src/cct/solana/validate.test.ts index 161a3d587..40c8f5276 100644 --- a/ccip-sdk/src/cct/solana/validate.test.ts +++ b/ccip-sdk/src/cct/solana/validate.test.ts @@ -23,6 +23,9 @@ import { validatePoolType, validatePublicKey, validatePublicKeys, + validateUniqueChainSelectors, + validateUniqueHexBytes, + validateUniquePublicKeys, validateWritableIndexes, } from './validate.ts' @@ -228,6 +231,51 @@ describe('Validate (cct/solana)', () => { ) }) + it('rejects duplicate public keys', () => { + const address = PublicKey.default + assert.throws( + () => validateUniquePublicKeys('op', 'addresses', [address, address]), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'addresses[1]', + ) + }) + + it('rejects duplicate chain selectors', () => { + assert.doesNotThrow(() => validateUniqueChainSelectors('op', 'selectors', [1n, 2n])) + assert.throws( + () => validateUniqueChainSelectors('op', 'selectors', [1n, 1n]), + (err: unknown) => + err instanceof CCTParamsInvalidError && err.context.param === 'selectors[1]', + ) + }) + + it('rejects duplicate hex byte values', () => { + assert.doesNotThrow(() => validateUniqueHexBytes('op', 'addresses', [Buffer.from('01', 'hex')])) + assert.throws( + () => + validateUniqueHexBytes('op', 'addresses', [ + Buffer.from('01', 'hex'), + Buffer.from('01', 'hex'), + ]), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'addresses[1]' && + err.context.reason === 'must not contain duplicate hex values', + ) + assert.throws( + () => + validateUniqueHexBytes( + 'op', + 'remotePoolAddresses', + [Buffer.from('01', 'hex'), Buffer.from('01', 'hex')], + 'remote pool addresses', + ), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.reason === 'must not contain duplicate remote pool addresses', + ) + }) + it('validates token delegation', () => { const tokenAccount = PublicKey.default const delegate = new PublicKey(Uint8Array.from({ length: 32 }, () => 1)) diff --git a/ccip-sdk/src/cct/solana/validate.ts b/ccip-sdk/src/cct/solana/validate.ts index 17052c88e..913031c64 100644 --- a/ccip-sdk/src/cct/solana/validate.ts +++ b/ccip-sdk/src/cct/solana/validate.ts @@ -85,6 +85,76 @@ export function validatePublicKeys(operation: string, param: string, values: unk for (const [i, value] of values.entries()) validatePublicKey(operation, `${param}[${i}]`, value) } +/** + * Asserts public keys do not contain duplicates. + * @throws CCTParamsInvalidError if a public key is duplicated. + */ +export function validateUniquePublicKeys( + operation: string, + param: string, + publicKeys: PublicKey[], +): void { + const seen = new Set() + for (const [i, publicKey] of publicKeys.entries()) { + const address = publicKey.toBase58() + if (seen.has(address)) { + throw new CCTParamsInvalidError( + operation, + `${param}[${i}]`, + 'must not contain duplicate addresses', + ) + } + seen.add(address) + } +} + +/** + * Asserts bigint chain selectors do not contain duplicates. + * @remarks Silently ignores non-bigint entries; relies on downstream `validateBigInt` for type safety. + * @throws CCTParamsInvalidError if a chain selector is duplicated. + */ +export function validateUniqueChainSelectors( + operation: string, + param: string, + selectors: unknown[], +): void { + const seen = new Set() + for (const [i, selector] of selectors.entries()) { + if (typeof selector === 'bigint' && seen.has(selector)) { + throw new CCTParamsInvalidError( + operation, + `${param}[${i}]`, + 'must not contain duplicate chain selectors', + ) + } + if (typeof selector === 'bigint') seen.add(selector) + } +} + +/** + * Asserts hex byte values do not contain duplicates. + * @throws CCTParamsInvalidError if a hex byte value is duplicated. + */ +export function validateUniqueHexBytes( + operation: string, + param: string, + values: Buffer[], + label = 'hex values', +): void { + const seen = new Set() + for (const [i, value] of values.entries()) { + const hex = value.toString('hex') + if (seen.has(hex)) { + throw new CCTParamsInvalidError( + operation, + `${param}[${i}]`, + `must not contain duplicate ${label}`, + ) + } + seen.add(hex) + } +} + /** * Asserts `value` is a non-empty string. * @throws CCTParamsInvalidError if `value` is not a non-empty string. @@ -347,7 +417,11 @@ export async function resolveExistingTokenAccount( tokenAddress: PublicKey, holder: PublicKey, tokenAccount?: PublicKey, -): Promise<{ tokenAccount: PublicKey; tokenProgram: PublicKey; account: Account }> { +): Promise<{ + tokenAccount: PublicKey + tokenProgram: PublicKey + account: Account +}> { const { ata, tokenProgram } = await resolveATA(connection, tokenAddress, holder) const account = tokenAccount ?? ata let tokenAccountInfo: Account diff --git a/ccip-sdk/src/gas.ts b/ccip-sdk/src/gas.ts index 738da11ae..957177c41 100644 --- a/ccip-sdk/src/gas.ts +++ b/ccip-sdk/src/gas.ts @@ -196,8 +196,7 @@ export async function getDestTokenAmount({ let sourceTokenAddress, sourcePoolAddress, destTokenAddress if ('destTokenAddress' in tokenAmount) { ;({ destTokenAddress, sourcePoolAddress, sourceTokenAddress } = tokenAmount) - } else if (!source) - return tokenAmount // if we don't have a source, assume we were already given a dest `{token, amount}` + } else if (!source) return tokenAmount // if we don't have a source, assume we were already given a dest `{token, amount}` else { ;({ destTokenAddress, sourceTokenAddress, sourcePoolAddress } = await sourceToDestTokenAddresses({ diff --git a/package-lock.json b/package-lock.json index b236d1852..b042817c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16102,7 +16102,20 @@ } }, "node_modules/image-size": { - "version": "2.0.2", + "name": "@localnerve/image-size", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@localnerve/image-size/-/image-size-2.1.2.tgz", + "integrity": "sha512-EiIL9ZlEyK32Upr+M5ufBTYCo/eHt8RXHyg09MCB0RVymbpibMZ/PHkrOM26ryM4mQ/MxF+pzYk58lJWTHIPtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/localnerve" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/ncp/payment/DHCB5GUYMGX5U" + } + ], "license": "MIT", "bin": { "image-size": "bin/image-size.js" From 398e87d81588e32efca74a0f61e619fea66bc970 Mon Sep 17 00:00:00 2001 From: Pedro Barbosa <14206093+apedrob@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:38:13 +0100 Subject: [PATCH 87/87] feat(cct-sdk): Add mint/burn role management ops (#402) * feat(cct-sdk): Add EVM mint/burn role management ops * tight tsdoc --- ccip-sdk/src/cct/evm/index.test.ts | 122 ++++++++ ccip-sdk/src/cct/evm/index.ts | 276 +++++++++++++++++ ccip-sdk/src/cct/evm/token/contracts.ts | 78 ++++- .../token/operations/grant-burn-role.test.ts | 260 ++++++++++++++++ .../evm/token/operations/grant-burn-role.ts | 80 +++++ .../grant-mint-and-burn-roles.test.ts | 277 ++++++++++++++++++ .../operations/grant-mint-and-burn-roles.ts | 90 ++++++ .../token/operations/grant-mint-role.test.ts | 260 ++++++++++++++++ .../evm/token/operations/grant-mint-role.ts | 80 +++++ .../token/operations/revoke-burn-role.test.ts | 260 ++++++++++++++++ .../evm/token/operations/revoke-burn-role.ts | 80 +++++ .../token/operations/revoke-mint-role.test.ts | 260 ++++++++++++++++ .../evm/token/operations/revoke-mint-role.ts | 80 +++++ 13 files changed, 2187 insertions(+), 16 deletions(-) create mode 100644 ccip-sdk/src/cct/evm/token/operations/grant-burn-role.test.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/grant-burn-role.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/grant-mint-and-burn-roles.test.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/grant-mint-and-burn-roles.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/grant-mint-role.test.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/grant-mint-role.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/revoke-burn-role.test.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/revoke-burn-role.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/revoke-mint-role.test.ts create mode 100644 ccip-sdk/src/cct/evm/token/operations/revoke-mint-role.ts diff --git a/ccip-sdk/src/cct/evm/index.test.ts b/ccip-sdk/src/cct/evm/index.test.ts index f0ce135e2..015467a27 100644 --- a/ccip-sdk/src/cct/evm/index.test.ts +++ b/ccip-sdk/src/cct/evm/index.test.ts @@ -723,6 +723,128 @@ describe('EVMTokenManager (cct/evm)', () => { }) }) + describe('mint/burn role management', () => { + const ROLE_TOKEN_OWNER = '0x' + '99'.repeat(20) + const ROLE_ACCOUNT = '0x' + 'aa'.repeat(20) + /** Fresh Interface — the manager's own cached one must not be what these assertions compare to. */ + const ROLES = new Interface([ + 'function grantMintAndBurnRoles(address burnAndMinter)', + 'function grantMintRole(address minter)', + 'function grantBurnRole(address burner)', + 'function revokeMintRole(address minter)', + 'function revokeBurnRole(address burner)', + 'function owner() view returns (address)', + 'function isMinter(address minter) view returns (bool)', + 'function isBurner(address burner) view returns (bool)', + ]) + + /** + * Chain stub for a v1.6.2 `FactoryBurnMintERC20` owned by `ROLE_TOKEN_OWNER`, on which + * `ROLE_ACCOUNT` holds `roles` — enough for both the owner gate and the role-state pre-flight. + */ + function roleChain(roles: { isMinter?: boolean; isBurner?: boolean } = {}) { + const results: Record = { + owner: [ROLE_TOKEN_OWNER], + isMinter: [roles.isMinter ?? false], + isBurner: [roles.isBurner ?? false], + } + return stubChain({ + provider: { + call: ({ data }: { data: string }) => { + const fn = ROLES.getFunction(data.slice(0, 10))!.name + return Promise.resolve(ROLES.encodeFunctionResult(fn, results[fn])) + }, + } as never, + }) + } + + /** + * One case per wired op: the two manager methods, the account parameter, and the role state + * that makes its call a real change. The methods are named explicitly rather than indexed by + * string, so a renamed or unwired method is a compile error here. + */ + const CASES = [ + { + fn: 'grantMintAndBurnRoles', + param: 'burnAndMinter', + roles: {}, + generate: (cct: EVMTokenManager, o: object) => + cct.generateUnsignedGrantMintAndBurnRoles(o as never), + submit: (cct: EVMTokenManager, o: object) => cct.grantMintAndBurnRoles(o as never), + }, + { + fn: 'grantMintRole', + param: 'minter', + roles: {}, + generate: (cct: EVMTokenManager, o: object) => + cct.generateUnsignedGrantMintRole(o as never), + submit: (cct: EVMTokenManager, o: object) => cct.grantMintRole(o as never), + }, + { + fn: 'grantBurnRole', + param: 'burner', + roles: {}, + generate: (cct: EVMTokenManager, o: object) => + cct.generateUnsignedGrantBurnRole(o as never), + submit: (cct: EVMTokenManager, o: object) => cct.grantBurnRole(o as never), + }, + { + fn: 'revokeMintRole', + param: 'minter', + roles: { isMinter: true }, + generate: (cct: EVMTokenManager, o: object) => + cct.generateUnsignedRevokeMintRole(o as never), + submit: (cct: EVMTokenManager, o: object) => cct.revokeMintRole(o as never), + }, + { + fn: 'revokeBurnRole', + param: 'burner', + roles: { isBurner: true }, + generate: (cct: EVMTokenManager, o: object) => + cct.generateUnsignedRevokeBurnRole(o as never), + submit: (cct: EVMTokenManager, o: object) => cct.revokeBurnRole(o as never), + }, + ] as const + + for (const { fn, param, roles, generate, submit } of CASES) { + const expected = ROLES.encodeFunctionData(fn, [ROLE_ACCOUNT]) + + it(`generateUnsigned* encodes ${fn}(address) to the token`, async () => { + const cct = EVMTokenManager.fromChain(roleChain(roles)) + const unsigned = await generate(cct, { + tokenAddress: TOKEN, + [param]: ROLE_ACCOUNT, + sender: ROLE_TOKEN_OWNER, + }) + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + const tx = unsigned.transactions[0]! + assert.equal(tx.to, TOKEN) + assert.equal(tx.from, ROLE_TOKEN_OWNER) + assert.equal(tx.data, expected) + }) + + it(`${fn} submits as the token owner`, async () => { + const cct = EVMTokenManager.fromChain(roleChain(roles)) + const { hash } = await submit(cct, { + tokenAddress: TOKEN, + [param]: ROLE_ACCOUNT, + wallet: fakeSigner(ROLE_TOKEN_OWNER), + }) + assert.equal(hash, HASH) + }) + + it(`${fn} rejects a sender that does not own the token`, async () => { + const cct = EVMTokenManager.fromChain(roleChain(roles)) + await assert.rejects( + () => generate(cct, { tokenAddress: TOKEN, [param]: ROLE_ACCOUNT, sender: ADMIN }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + } + }) + describe('mint and role reads', () => { const MINTER = '0x' + '99'.repeat(20) const RECIPIENT = '0x' + 'aa'.repeat(20) diff --git a/ccip-sdk/src/cct/evm/index.ts b/ccip-sdk/src/cct/evm/index.ts index 48b11e18a..811e32bef 100644 --- a/ccip-sdk/src/cct/evm/index.ts +++ b/ccip-sdk/src/cct/evm/index.ts @@ -98,9 +98,17 @@ import { type GetMintersResult, GetMinters, } from './token/operations/get-minters.ts' +import { type GrantBurnRoleParams, GrantBurnRole } from './token/operations/grant-burn-role.ts' +import { + type GrantMintAndBurnRolesParams, + GrantMintAndBurnRoles, +} from './token/operations/grant-mint-and-burn-roles.ts' +import { type GrantMintRoleParams, GrantMintRole } from './token/operations/grant-mint-role.ts' import { type IsBurnerParams, type IsBurnerResult, IsBurner } from './token/operations/is-burner.ts' import { type IsMinterParams, type IsMinterResult, IsMinter } from './token/operations/is-minter.ts' import { type MintParams, Mint } from './token/operations/mint.ts' +import { type RevokeBurnRoleParams, RevokeBurnRole } from './token/operations/revoke-burn-role.ts' +import { type RevokeMintRoleParams, RevokeMintRole } from './token/operations/revoke-mint-role.ts' /** CCT admin operations for EVM chains, delegating each op to an operation class. */ export class EVMTokenManager extends TokenManager { @@ -108,6 +116,11 @@ export class EVMTokenManager extends TokenManager { // Token operations readonly #deployToken = new DeployToken() readonly #mint = new Mint() + readonly #grantMintAndBurnRoles = new GrantMintAndBurnRoles() + readonly #grantMintRole = new GrantMintRole() + readonly #grantBurnRole = new GrantBurnRole() + readonly #revokeMintRole = new RevokeMintRole() + readonly #revokeBurnRole = new RevokeBurnRole() readonly #getMinters = new GetMinters() readonly #getBurners = new GetBurners() readonly #isMinter = new IsMinter() @@ -695,6 +708,264 @@ export class EVMTokenManager extends TokenManager { return this.#deployToken.execute(this.chain, opts) } + /** + * Builds an unsigned `grantMintAndBurnRoles` tx (for multisig / offline signing): grants a + * BurnMintERC677 token's mint **and** burn roles to one account, in a single transaction. This + * is the call that lets a freshly deployed burn/mint pool bridge the token. + * @remarks v1.5.1 / v1.6.2 tokens only — v2.0.0's `CrossChainToken` gates mint/burn through + * AccessControl, which ships separately. Rejected only when `burnAndMinter` already holds + * *both* roles; holding just one still builds, since this call is what completes the pair. + * @remarks {@link deployToken} deploys v2.0.0, so it is not a source of a token these ops + * accept: a v1.5.1 / v1.6.2 `FactoryBurnMintERC20` comes from the CCIP token factory or your + * own deployment, outside this SDK. + * @see {@link deployTokenPool} — the primary use case is granting these roles to a freshly + * deployed pool + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the token owner, or `burnAndMinter` already holds both roles + * @example + * ```typescript + * // build only — sign later (multisig / offline). `sender` must be the token owner. + * const unsigned = await cct.generateUnsignedGrantMintAndBurnRoles({ + * tokenAddress: '0xToken...', + * burnAndMinter: '0xPool...', // the token's burn/mint pool + * sender: '0xTokenOwner...', + * }) + * ``` + */ + generateUnsignedGrantMintAndBurnRoles(opts: GrantMintAndBurnRolesParams): Promise { + return this.#grantMintAndBurnRoles.generate(this.chain, opts) + } + + /** + * Grants a BurnMintERC677 token's mint and burn roles to one account, signing + submitting with + * `opts.wallet` (the token owner). + * @remarks See {@link generateUnsignedGrantMintAndBurnRoles} for the version and redundancy + * rules. `sender` defaults to the wallet's address, so the owner gate always runs before this + * submits. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the wallet's address, the wallet is not the token owner, or `burnAndMinter` already holds + * both roles + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.grantMintAndBurnRoles({ + * tokenAddress: '0xToken...', + * burnAndMinter: '0xPool...', + * wallet, // must be the token owner + * }) + * ``` + */ + grantMintAndBurnRoles( + opts: EVMExecuteParams, + ): Promise { + return this.#grantMintAndBurnRoles.execute(this.chain, opts) + } + + /** + * Builds an unsigned `grantMintRole` tx (for multisig / offline signing): grants a + * BurnMintERC677 token's mint role to one account. Pair it with + * {@link generateUnsignedGrantBurnRole}, or use + * {@link generateUnsignedGrantMintAndBurnRoles} to grant both in one transaction. + * @remarks v1.5.1 / v1.6.2 tokens only; a redundant grant is rejected, since the chain would + * mine it as a silent no-op rather than revert. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the token owner, or `minter` already holds the mint role + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedGrantMintRole({ + * tokenAddress: '0xToken...', + * minter: '0xMinter...', + * sender: '0xTokenOwner...', + * }) + * ``` + * @see {@link deployTokenPool} — the primary use case is granting this role to a freshly + * deployed pool + */ + generateUnsignedGrantMintRole(opts: GrantMintRoleParams): Promise { + return this.#grantMintRole.generate(this.chain, opts) + } + + /** + * Grants a BurnMintERC677 token's mint role to one account, signing + submitting with + * `opts.wallet` (the token owner). + * @remarks See {@link generateUnsignedGrantMintRole} for the version and redundancy rules. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the wallet's address, the wallet is not the token owner, or `minter` already holds the role + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.grantMintRole({ + * tokenAddress: '0xToken...', + * minter: '0xMinter...', + * wallet, // must be the token owner + * }) + * ``` + */ + grantMintRole(opts: EVMExecuteParams): Promise { + return this.#grantMintRole.execute(this.chain, opts) + } + + /** + * Builds an unsigned `grantBurnRole` tx (for multisig / offline signing): grants a + * BurnMintERC677 token's burn role to one account. Pair it with + * {@link generateUnsignedGrantMintRole}, or use + * {@link generateUnsignedGrantMintAndBurnRoles} to grant both in one transaction. + * @remarks v1.5.1 / v1.6.2 tokens only; a redundant grant is rejected — see + * {@link generateUnsignedGrantMintRole}. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the token owner, or `burner` already holds the burn role + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedGrantBurnRole({ + * tokenAddress: '0xToken...', + * burner: '0xBurner...', + * sender: '0xTokenOwner...', + * }) + * ``` + * @see {@link deployTokenPool} — the primary use case is granting this role to a freshly + * deployed pool + */ + generateUnsignedGrantBurnRole(opts: GrantBurnRoleParams): Promise { + return this.#grantBurnRole.generate(this.chain, opts) + } + + /** + * Grants a BurnMintERC677 token's burn role to one account, signing + submitting with + * `opts.wallet` (the token owner). + * @remarks See {@link generateUnsignedGrantBurnRole} for the version and redundancy rules. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the wallet's address, the wallet is not the token owner, or `burner` already holds the role + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.grantBurnRole({ + * tokenAddress: '0xToken...', + * burner: '0xBurner...', + * wallet, // must be the token owner + * }) + * ``` + */ + grantBurnRole(opts: EVMExecuteParams): Promise { + return this.#grantBurnRole.execute(this.chain, opts) + } + + /** + * Builds an unsigned `revokeMintRole` tx (for multisig / offline signing): removes a + * BurnMintERC677 token's mint role from one account. + * @remarks v1.5.1 / v1.6.2 tokens only; revoking a role the account does not hold is rejected, + * since the chain would mine it as a silent no-op and tell you nothing. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the token owner, or `minter` does not currently hold the mint role + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedRevokeMintRole({ + * tokenAddress: '0xToken...', + * minter: '0xOldPool...', // must currently hold the role + * sender: '0xTokenOwner...', + * }) + * ``` + * @see {@link deployTokenPool} — the mirror of the grant made to a freshly deployed pool + */ + generateUnsignedRevokeMintRole(opts: RevokeMintRoleParams): Promise { + return this.#revokeMintRole.generate(this.chain, opts) + } + + /** + * Removes a BurnMintERC677 token's mint role from one account, signing + submitting with + * `opts.wallet` (the token owner). + * @remarks See {@link generateUnsignedRevokeMintRole} for the version and role-state rules. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the wallet's address, the wallet is not the token owner, or `minter` does not hold the role + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.revokeMintRole({ + * tokenAddress: '0xToken...', + * minter: '0xOldPool...', + * wallet, // must be the token owner + * }) + * ``` + */ + revokeMintRole(opts: EVMExecuteParams): Promise { + return this.#revokeMintRole.execute(this.chain, opts) + } + + /** + * Builds an unsigned `revokeBurnRole` tx (for multisig / offline signing): removes a + * BurnMintERC677 token's burn role from one account. + * @remarks v1.5.1 / v1.6.2 tokens only; revoking a role the account does not hold is rejected — + * see {@link generateUnsignedRevokeMintRole}. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the token owner, or `burner` does not currently hold the burn role + * @example + * ```typescript + * const unsigned = await cct.generateUnsignedRevokeBurnRole({ + * tokenAddress: '0xToken...', + * burner: '0xOldPool...', // must currently hold the role + * sender: '0xTokenOwner...', + * }) + * ``` + * @see {@link deployTokenPool} — the mirror of the grant made to a freshly deployed pool + */ + generateUnsignedRevokeBurnRole(opts: RevokeBurnRoleParams): Promise { + return this.#revokeBurnRole.generate(this.chain, opts) + } + + /** + * Removes a BurnMintERC677 token's burn role from one account, signing + submitting with + * `opts.wallet` (the token owner). + * @remarks See {@link generateUnsignedRevokeBurnRole} for the version and role-state rules. + * @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * (a v2.0.0 `CrossChainToken` included, since it gates mint/burn through AccessControl) + * @throws {@link CCTParamsInvalidError} if any param is invalid, `sender` is given and is not + * the wallet's address, the wallet is not the token owner, or `burner` does not hold the role + * @throws {@link CCIPExecTxRevertedError} if the tx reverts on-chain + * @throws {@link CCTTxFailedError} if submission fails before broadcast + * @throws {@link CCTTxNotConfirmedError} if it is not confirmed in time + * @example + * ```typescript + * const { hash } = await cct.revokeBurnRole({ + * tokenAddress: '0xToken...', + * burner: '0xOldPool...', + * wallet, // must be the token owner + * }) + * ``` + */ + revokeBurnRole(opts: EVMExecuteParams): Promise { + return this.#revokeBurnRole.execute(this.chain, opts) + } + /** * Builds an unsigned `mint` tx (for multisig / offline signing): mints new supply of a * BurnMintERC677 token to `account`. The manual mint — seeding liquidity, topping up test @@ -1434,6 +1705,11 @@ export type { } from './token-admin-registry/operations/get-supported-tokens.ts' export * from './token-admin-registry/contracts.ts' export type { DeployTokenParams } from './token/operations/deploy-token.ts' +export type { GrantMintAndBurnRolesParams } from './token/operations/grant-mint-and-burn-roles.ts' +export type { GrantMintRoleParams } from './token/operations/grant-mint-role.ts' +export type { GrantBurnRoleParams } from './token/operations/grant-burn-role.ts' +export type { RevokeMintRoleParams } from './token/operations/revoke-mint-role.ts' +export type { RevokeBurnRoleParams } from './token/operations/revoke-burn-role.ts' export type { MintParams } from './token/operations/mint.ts' export type { GetMintersParams, GetMintersResult } from './token/operations/get-minters.ts' export type { GetBurnersParams, GetBurnersResult } from './token/operations/get-burners.ts' diff --git a/ccip-sdk/src/cct/evm/token/contracts.ts b/ccip-sdk/src/cct/evm/token/contracts.ts index 81e823b2e..3f0b10124 100644 --- a/ccip-sdk/src/cct/evm/token/contracts.ts +++ b/ccip-sdk/src/cct/evm/token/contracts.ts @@ -1,10 +1,11 @@ /** * EVM token contract layer for CCT: cached {@link Interface}s per {@link TokenVersion} * ({@link getTokenInterface}) for read/write (e.g. ownership) ops, the deployable - * `CrossChainToken` (v2.0.0) artifact ({@link getTokenArtifact}), and the token's role reads — - * the narrow predicate a role-gated write pre-flights ({@link readTokenRole}) and the - * informational role-set enumerations ({@link readTokenRoleHolders}). `2.0.0` is - * `CrossChainToken`; `1.5.1` / `1.6.2` are `FactoryBurnMintERC20`. Mirrors + * `CrossChainToken` (v2.0.0) artifact ({@link getTokenArtifact}), the token's role reads — the + * narrow predicate a role-gated write pre-flights ({@link readTokenRole}) and the informational + * role-set enumerations ({@link readTokenRoleHolders}) — and the owner read + * ({@link readTokenOwner}) plus the owner-only guard over it ({@link assertTokenOwner}). `2.0.0` + * is `CrossChainToken`; `1.5.1` / `1.6.2` are `FactoryBurnMintERC20`. Mirrors * `token-pool/contracts.ts`. * * @packageDocumentation @@ -14,7 +15,12 @@ import { Interface, getAddress, isError } from 'ethers' import type { TypedContract } from 'ethers-abitype' import type { EVMChain } from '../../../evm/index.ts' -import { CCTContractTypeInvalidError, CCTContractVersionUnsupportedError } from '../../errors.ts' +import { resultToObject } from '../../../evm/types.ts' +import { + CCTContractTypeInvalidError, + CCTContractVersionUnsupportedError, + CCTParamsInvalidError, +} 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' @@ -51,6 +57,17 @@ export function getTokenInterface(version: TokenVersion): Interface { return TOKEN_INTERFACES[version] } +/** + * The interface every BurnMintERC677 role/mint write encodes through. + * + * Pinned to v1.5.1: the role functions, `mint`, and the role reads are identical at v1.6.2 and + * on `HyperLiquidCompatibleERC20 1.6.2`, so there is nothing to dispatch on. v2.0.0's + * `CrossChainToken` is a different contract, ruled out by {@link readTokenRole}. + */ +export function getErc20Token(): Interface { + return TOKEN_INTERFACES[TokenVersion.V1_5_1] +} + /** * Deploy artifacts ({@link DeployArtifact}: contract name + ctor {@link Interface} + creation * bytecode) keyed by {@link TokenVersion}, built once; read via {@link getTokenArtifact}. Only @@ -74,17 +91,6 @@ export function getTokenArtifact(version: TokenVersion): DeployArtifact { return artifact } -/** - * The interface every BurnMintERC677 role/mint write encodes through. - * - * Pinned to v1.5.1: the role functions, `mint`, and the role reads are identical at v1.6.2 and - * on `HyperLiquidCompatibleERC20 1.6.2`, so there is nothing to dispatch on. v2.0.0's - * `CrossChainToken` is a different contract, ruled out by {@link readTokenRole}. - */ -export function getErc20Token(): Interface { - return TOKEN_INTERFACES[TokenVersion.V1_5_1] -} - /** * True for the two failure shapes a call to a function a contract does not declare produces: * `CALL_EXCEPTION` (revert) and `BAD_DATA` (node answers `0x`). Deliberately narrow — a transport @@ -186,3 +192,43 @@ export async function readTokenRoleHolders( ) } } + +/** `Ownable2Step.owner()`, declared identically by every supported token. */ +type TokenOwnerGetter = Pick, 'owner'> + +/** + * Reads a token's Ownable2Step `owner()` in a single `eth_call`. On the BurnMintERC677 family the + * owner *is* the mint/burn role admin — `grantMintRole` and its siblings are `onlyOwner`. + * @param chain - Chain to read from. + * @param tokenAddress - Token contract to read `owner()` from. + * @returns The current owner, checksummed. + */ +export async function readTokenOwner(chain: EVMChain, tokenAddress: string): Promise { + const token: TokenOwnerGetter = getTypedContract( + chain, + tokenAddress, + FACTORY_BURN_MINT_ERC20_V1_5_1_ABI, + ) + return getAddress(resultToObject(await token.owner())) +} + +/** + * Pre-flights `sender` against the token's on-chain `owner()` for an owner-gated write, so an + * unauthorized caller fails as a {@link CCTParamsInvalidError} here instead of as an opaque + * `OnlyOwner` revert after a multisig has already reviewed and signed. + * @param operation - Operation name, for the error's `operation` field. + * @param chain - Chain to read the owner from. + * @param tokenAddress - Token being written to. + * @param sender - The address the tx will be sent from; compared checksummed. + * @throws {@link CCTParamsInvalidError} if `sender` is not the token owner + */ +export async function assertTokenOwner( + operation: string, + chain: EVMChain, + tokenAddress: string, + sender: string, +): Promise { + const owner = await readTokenOwner(chain, tokenAddress) + if (getAddress(sender) === owner) return + throw new CCTParamsInvalidError(operation, 'sender', `must be the current token owner (${owner})`) +} diff --git a/ccip-sdk/src/cct/evm/token/operations/grant-burn-role.test.ts b/ccip-sdk/src/cct/evm/token/operations/grant-burn-role.test.ts new file mode 100644 index 000000000..0f6169a57 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/grant-burn-role.test.ts @@ -0,0 +1,260 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../../../errors.ts' +import { type GrantBurnRoleParams, GrantBurnRole } from './grant-burn-role.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const OWNER = '0x' + '22'.repeat(20) +const ACCOUNT = '0x' + '33'.repeat(20) +const NOT_THE_OWNER = '0x' + '44'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +/** Calldata built from a fresh Interface — never the SDK's cached one, or this proves nothing. */ +const FRESH = new Interface([ + 'function grantBurnRole(address burner)', + 'function isBurner(address burner) view returns (bool)', + 'function owner() view returns (address)', +]) +const expectedData = (burner = ACCOUNT) => FRESH.encodeFunctionData('grantBurnRole', [burner]) + +/** The `eth_call`s the op makes, in order, as decoded function names. */ +type Seen = { calls: string[] } +const newSeen = (): Seen => ({ calls: [] }) + +/** + * EVMChain stub for a BurnMintERC677 token owned by `OWNER`. `holdsRole` is the pre-existing + * role state of the account being granted — false is the state that makes this call a real change. + */ +function stubChain({ + holdsRole = false, + owner = OWNER, + callError, + seen = newSeen(), +}: { + holdsRole?: boolean + owner?: string + /** Fails every `eth_call`, standing in for a contract that is not a BurnMintERC677 token. */ + callError?: Error + seen?: Seen +} = {}): EVMChain { + const results: Record = { isBurner: [holdsRole], owner: [owner] } + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + if (callError) return Promise.reject(callError) + const fn = FRESH.getFunction(data.slice(0, 10))!.name + seen.calls.push(fn) + return Promise.resolve(FRESH.encodeFunctionResult(fn, results[fn])) + }, + }, + nextNonce: () => Promise.resolve(0), + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(waitError?: Error, address = OWNER) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => (waitError ? Promise.reject(waitError) : Promise.resolve({ status: 1 })), + }), + } +} + +/** A revert with no data, the shape a call to an undeclared function produces. */ +const missingFunction = () => + makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + +const op = new GrantBurnRole() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { tokenAddress: TOKEN, burner: ACCOUNT, sender: OWNER, ...overrides }) +} + +describe('GrantBurnRole (cct/evm)', () => { + describe('generate', () => { + it('encodes grantBurnRole(address) to the token', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, TOKEN) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, expectedData()) + // the role read comes first: it is also the family check, so it gates the owner read + assert.deepEqual(seen.calls, ['isBurner', 'owner']) + }) + + it('omits from when sender is not supplied, but still probes the token', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen }), { sender: undefined }) + + assert.equal(unsigned.transactions[0]!.from, undefined) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + // no sender to compare, so the owner read is skipped — the family check is not + assert.deepEqual(seen.calls, ['isBurner']) + }) + }) + + describe('validation', () => { + for (const [param, value] of [ + ['tokenAddress', 'not-an-address'], + // a tx to `0x0` hits no code, so it mines as a successful no-op instead of reverting + ['tokenAddress', ZeroAddress], + ['burner', 'not-an-address'], + ['burner', ZeroAddress], + ['sender', 'not-an-address'], + ] as const) { + it(`rejects ${param} = ${String(value)} before any RPC`, async () => { + const seen = newSeen() + await assert.rejects( + () => generate(stubChain({ seen }), { [param]: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'grantBurnRole' && + err.context.param === param, + ) + assert.deepEqual(seen.calls, []) + }) + } + }) + + describe('family check', () => { + it('rejects a contract that is not a BurnMintERC677 token', async () => { + // a v2.0.0 CrossChainToken, a token pool, and an EOA all fail the isBurner read + await assert.rejects( + () => generate(stubChain({ callError: missingFunction() })), + (err: unknown) => + err instanceof CCTContractTypeInvalidError && err.context.address === TOKEN, + ) + }) + + it('rejects an unrelated address even with no sender to check', async () => { + // the case an owner gate alone would miss: a token pool declares owner() too + await assert.rejects( + () => generate(stubChain({ callError: missingFunction() }), { sender: undefined }), + (err: unknown) => err instanceof CCTContractTypeInvalidError, + ) + }) + }) + + describe('no-op guard', () => { + it('rejects a grant when the account already holds the burn role', async () => { + // stricter than the chain: the role set is an EnumerableSet, so this would mine as a + // silent no-op rather than revert + const seen = newSeen() + await assert.rejects( + () => generate(stubChain({ holdsRole: true, seen })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'grantBurnRole' && + err.context.param === 'burner' && + /already holds the burn role/.test(String(err.context.reason)), + ) + // rejected on the role read alone, before the owner read + assert.deepEqual(seen.calls, ['isBurner']) + }) + + it('rejects the no-op with no sender supplied too', async () => { + await assert.rejects( + () => generate(stubChain({ holdsRole: true }), { sender: undefined }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'burner', + ) + }) + }) + + describe('owner gate', () => { + it('rejects a sender that does not own the token', async () => { + await assert.rejects( + () => generate(stubChain(), { sender: NOT_THE_OWNER }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + /must be the current token owner/.test(String(err.context.reason)), + ) + }) + }) + + describe('execute', () => { + it('submits as the token owner and returns the tx hash', async () => { + const { hash } = await op.execute(stubChain(), { + tokenAddress: TOKEN, + burner: ACCOUNT, + wallet: fakeSigner(), + }) + assert.equal(hash, HASH) + }) + + it('rejects a sender that is not the signing wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + burner: ACCOUNT, + sender: NOT_THE_OWNER, + wallet: fakeSigner(), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a wallet that does not own the token', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + burner: ACCOUNT, + wallet: fakeSigner(undefined, NOT_THE_OWNER), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('surfaces an on-chain revert', async () => { + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'sendTransaction', + data: '0x', + reason: 'OnlyOwner', + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + burner: ACCOUNT, + wallet: fakeSigner(revert), + }), + (err: unknown) => err instanceof CCIPExecTxRevertedError, + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { tokenAddress: TOKEN, burner: ACCOUNT, wallet: {} }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/grant-burn-role.ts b/ccip-sdk/src/cct/evm/token/operations/grant-burn-role.ts new file mode 100644 index 000000000..7517355e1 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/grant-burn-role.ts @@ -0,0 +1,80 @@ +/** + * grantBurnRole: grants a BurnMintERC677 token's burn role to one account. Owner-gated + * (`onlyOwner`); the owner is the token's mint/burn role admin. + * + * @packageDocumentation + */ + +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 { validateNonZeroAddress } from '../../validate.ts' +import { assertTokenOwner, getErc20Token, readTokenRole } from '../contracts.ts' + +/** Parameters for {@link GrantBurnRole}. */ +export type GrantBurnRoleParams = { + /** BurnMintERC677 token (v1.5.1 / v1.6.2) whose roles are being changed. */ + tokenAddress: string + /** Account receiving the burn role; must not already hold it. */ + burner: string + /** Current token owner (the role admin); sets `tx.from` for offline / multisig signing. */ + sender?: string +} + +/** Grants the burn role on a BurnMintERC677 token via `grantBurnRole`. */ +export class GrantBurnRole extends EVMOperation { + readonly name = 'grantBurnRole' + + /** + * Validates both addresses before any RPC. Neither may be zero: a tx to `0x0` hits no code, and + * granting a role to `0x0` mines as a no-op nobody can use. + */ + protected override validate({ tokenAddress, burner }: GrantBurnRoleParams): void { + validateNonZeroAddress(this.name, 'tokenAddress', tokenAddress) + validateNonZeroAddress(this.name, 'burner', burner) + } + + /** + * Reads the current role state, then — when `sender` is known — confirms it owns the token. + * + * The role read runs first because it is also the family check ({@link readTokenRole}), which + * `owner()` cannot make: a token pool and a v2.0.0 `CrossChainToken` declare `owner()` too. Both + * checks run here rather than in {@link execute}, so the offline / multisig path gets them, and + * a redundant grant is rejected even though the chain would mine it as a silent no-op. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * @throws {@link CCTParamsInvalidError} if `burner` already holds the burn role, or `sender` + * is given and is not the token owner + */ + protected async buildUnsigned( + chain: EVMChain, + { tokenAddress, burner, sender }: GrantBurnRoleParams, + ): Promise { + if (await readTokenRole(chain, tokenAddress, 'isBurner', burner)) + throw new CCTParamsInvalidError( + this.name, + 'burner', + `already holds the burn role on ${tokenAddress}; granting it again changes nothing`, + ) + if (sender !== undefined) await assertTokenOwner(this.name, chain, tokenAddress, sender) + + return callTx(tokenAddress, getErc20Token().encodeFunctionData('grantBurnRole', [burner])) + } + + /** + * Signs and submits as the token owner, defaulting `sender` to the signing wallet — the only + * address that can satisfy {@link buildUnsigned}'s owner check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} 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, + * or if any other param is invalid (see {@link buildUnsigned}) + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/grant-mint-and-burn-roles.test.ts b/ccip-sdk/src/cct/evm/token/operations/grant-mint-and-burn-roles.test.ts new file mode 100644 index 000000000..e50a745d3 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/grant-mint-and-burn-roles.test.ts @@ -0,0 +1,277 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../../../errors.ts' +import { + type GrantMintAndBurnRolesParams, + GrantMintAndBurnRoles, +} from './grant-mint-and-burn-roles.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const OWNER = '0x' + '22'.repeat(20) +const POOL = '0x' + '33'.repeat(20) +const NOT_THE_OWNER = '0x' + '44'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +/** Calldata built from a fresh Interface — never the SDK's cached one, or this proves nothing. */ +const FRESH = new Interface([ + 'function grantMintAndBurnRoles(address burnAndMinter)', + 'function isMinter(address minter) view returns (bool)', + 'function isBurner(address burner) view returns (bool)', + 'function owner() view returns (address)', +]) +const expectedData = (burnAndMinter = POOL) => + FRESH.encodeFunctionData('grantMintAndBurnRoles', [burnAndMinter]) + +/** The `eth_call`s the op makes, as decoded function names. The two role reads race, so unordered. */ +type Seen = { calls: string[] } +const newSeen = (): Seen => ({ calls: [] }) + +/** + * EVMChain stub for a BurnMintERC677 token owned by `OWNER`, on which `burnAndMinter` already + * holds `roles`. Defaults to holding neither — the fresh-pool case this op exists for. + */ +function stubChain({ + roles = {}, + owner = OWNER, + callError, + seen = newSeen(), +}: { + roles?: { isMinter?: boolean; isBurner?: boolean } + owner?: string + /** Fails every `eth_call`, standing in for a contract that is not a BurnMintERC677 token. */ + callError?: Error + seen?: Seen +} = {}): EVMChain { + const results: Record = { + isMinter: [roles.isMinter ?? false], + isBurner: [roles.isBurner ?? false], + owner: [owner], + } + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + if (callError) return Promise.reject(callError) + const fn = FRESH.getFunction(data.slice(0, 10))!.name + seen.calls.push(fn) + return Promise.resolve(FRESH.encodeFunctionResult(fn, results[fn])) + }, + }, + nextNonce: () => Promise.resolve(0), + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(waitError?: Error, address = OWNER) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => (waitError ? Promise.reject(waitError) : Promise.resolve({ status: 1 })), + }), + } +} + +/** A revert with no data, the shape a call to an undeclared function produces. */ +const missingFunction = () => + makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + +const op = new GrantMintAndBurnRoles() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { + tokenAddress: TOKEN, + burnAndMinter: POOL, + sender: OWNER, + ...overrides, + }) +} + +describe('GrantMintAndBurnRoles (cct/evm)', () => { + describe('generate', () => { + it('encodes grantMintAndBurnRoles(address) to the token', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + // one native on-chain function, so one tx — not a grantMintRole + grantBurnRole pair + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, TOKEN) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, expectedData()) + assert.deepEqual(seen.calls.slice(0, 2).sort(), ['isBurner', 'isMinter']) + assert.equal(seen.calls[2], 'owner') + }) + + it('omits from when sender is not supplied, but still probes the token', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen }), { sender: undefined }) + + assert.equal(unsigned.transactions[0]!.from, undefined) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + // no sender to compare, so the owner read is skipped — the family check is not + assert.deepEqual(seen.calls.sort(), ['isBurner', 'isMinter']) + }) + }) + + describe('validation', () => { + for (const [param, value] of [ + ['tokenAddress', 'not-an-address'], + // a tx to `0x0` hits no code, so it mines as a successful no-op instead of reverting + ['tokenAddress', ZeroAddress], + ['burnAndMinter', 'not-an-address'], + ['burnAndMinter', ZeroAddress], + ['sender', 'not-an-address'], + ] as const) { + it(`rejects ${param} = ${String(value)} before any RPC`, async () => { + const seen = newSeen() + await assert.rejects( + () => generate(stubChain({ seen }), { [param]: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'grantMintAndBurnRoles' && + err.context.param === param, + ) + assert.deepEqual(seen.calls, []) + }) + } + }) + + describe('family check', () => { + it('rejects a contract that is not a BurnMintERC677 token', async () => { + // v2.0.0's CrossChainToken declares grantMintAndBurnRoles too, but gates it through + // AccessControl — the isMinter/isBurner reads are what tell the two apart + await assert.rejects( + () => generate(stubChain({ callError: missingFunction() })), + (err: unknown) => + err instanceof CCTContractTypeInvalidError && err.context.address === TOKEN, + ) + }) + + it('rejects an unrelated address even with no sender to check', async () => { + // the case an owner gate alone would miss: a token pool declares owner() too + await assert.rejects( + () => generate(stubChain({ callError: missingFunction() }), { sender: undefined }), + (err: unknown) => err instanceof CCTContractTypeInvalidError, + ) + }) + }) + + describe('no-op guard', () => { + it('rejects an account that already holds both roles', async () => { + // stricter than the chain: the role sets are EnumerableSets, so this would mine as a + // silent no-op rather than revert + const seen = newSeen() + await assert.rejects( + () => generate(stubChain({ roles: { isMinter: true, isBurner: true }, seen })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'grantMintAndBurnRoles' && + err.context.param === 'burnAndMinter' && + /already holds the mint and burn roles/.test(String(err.context.reason)), + ) + // rejected on the role reads alone, before the owner read + assert.ok(!seen.calls.includes('owner')) + }) + + for (const roles of [{ isMinter: true }, { isBurner: true }] as const) { + const held = 'isMinter' in roles ? 'mint' : 'burn' + it(`builds for an account holding only the ${held} role — completing the pair is the point`, async () => { + const unsigned = await generate(stubChain({ roles })) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + }) + } + }) + + describe('owner gate', () => { + it('rejects a sender that does not own the token', async () => { + await assert.rejects( + () => generate(stubChain(), { sender: NOT_THE_OWNER }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + /must be the current token owner/.test(String(err.context.reason)), + ) + }) + }) + + describe('execute', () => { + it('submits as the token owner and returns the tx hash', async () => { + const { hash } = await op.execute(stubChain(), { + tokenAddress: TOKEN, + burnAndMinter: POOL, + wallet: fakeSigner(), + }) + assert.equal(hash, HASH) + }) + + it('rejects a sender that is not the signing wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + burnAndMinter: POOL, + sender: NOT_THE_OWNER, + wallet: fakeSigner(), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a wallet that does not own the token', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + burnAndMinter: POOL, + wallet: fakeSigner(undefined, NOT_THE_OWNER), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('surfaces an on-chain revert', async () => { + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'sendTransaction', + data: '0x', + reason: 'OnlyOwner', + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + burnAndMinter: POOL, + wallet: fakeSigner(revert), + }), + (err: unknown) => err instanceof CCIPExecTxRevertedError, + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { tokenAddress: TOKEN, burnAndMinter: POOL, wallet: {} }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/grant-mint-and-burn-roles.ts b/ccip-sdk/src/cct/evm/token/operations/grant-mint-and-burn-roles.ts new file mode 100644 index 000000000..16865cc69 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/grant-mint-and-burn-roles.ts @@ -0,0 +1,90 @@ +/** + * grantMintAndBurnRoles: grants a BurnMintERC677 token's mint *and* burn roles to one account in + * a single transaction — the call a token owner makes for a newly deployed pool, which needs both. + * Owner-gated (`onlyOwner`); the owner is the token's mint/burn role admin. + * + * @packageDocumentation + */ + +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 { validateNonZeroAddress } from '../../validate.ts' +import { assertTokenOwner, getErc20Token, readTokenRole } from '../contracts.ts' + +/** Parameters for {@link GrantMintAndBurnRoles}. */ +export type GrantMintAndBurnRolesParams = { + /** BurnMintERC677 token (v1.5.1 / v1.6.2) whose roles are being changed. */ + tokenAddress: string + /** Account receiving both roles, typically the token's pool; must not already hold both. */ + burnAndMinter: string + /** Current token owner (the role admin); sets `tx.from` for offline / multisig signing. */ + sender?: string +} + +/** Grants both mint and burn roles on a BurnMintERC677 token via `grantMintAndBurnRoles`. */ +export class GrantMintAndBurnRoles extends EVMOperation { + readonly name = 'grantMintAndBurnRoles' + + /** + * Validates both addresses before any RPC. Neither may be zero: a tx to `0x0` hits no code, and + * granting roles to `0x0` mines as a no-op nobody can use. + */ + protected override validate({ tokenAddress, burnAndMinter }: GrantMintAndBurnRolesParams): void { + validateNonZeroAddress(this.name, 'tokenAddress', tokenAddress) + validateNonZeroAddress(this.name, 'burnAndMinter', burnAndMinter) + } + + /** + * Reads both role states, then — when `sender` is known — confirms it owns the token. + * Rejected only when the account holds both roles already: holding one still builds, since + * completing the pair is what this call is for. + * + * The role reads run first because they are also the family check ({@link readTokenRole}), which + * `owner()` cannot make: a token pool and a v2.0.0 `CrossChainToken` declare `owner()` too, and + * v2.0.0 declares `grantMintAndBurnRoles` itself. Both checks run here rather than in + * {@link execute}, so the offline / multisig path gets them. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * @throws {@link CCTParamsInvalidError} if `burnAndMinter` already holds both roles, or `sender` + * is given and is not the token owner + */ + protected async buildUnsigned( + chain: EVMChain, + { tokenAddress, burnAndMinter, sender }: GrantMintAndBurnRolesParams, + ): Promise { + const [isMinter, isBurner] = await Promise.all([ + readTokenRole(chain, tokenAddress, 'isMinter', burnAndMinter), + readTokenRole(chain, tokenAddress, 'isBurner', burnAndMinter), + ]) + if (isMinter && isBurner) + throw new CCTParamsInvalidError( + this.name, + 'burnAndMinter', + `already holds the mint and burn roles on ${tokenAddress}; granting them again changes nothing`, + ) + if (sender !== undefined) await assertTokenOwner(this.name, chain, tokenAddress, sender) + + return callTx( + tokenAddress, + getErc20Token().encodeFunctionData('grantMintAndBurnRoles', [burnAndMinter]), + ) + } + + /** + * Signs and submits as the token owner, defaulting `sender` to the signing wallet — the only + * address that can satisfy {@link buildUnsigned}'s owner check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} 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, or + * if any other param is invalid (see {@link buildUnsigned}) + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/grant-mint-role.test.ts b/ccip-sdk/src/cct/evm/token/operations/grant-mint-role.test.ts new file mode 100644 index 000000000..8b045fcac --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/grant-mint-role.test.ts @@ -0,0 +1,260 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../../../errors.ts' +import { type GrantMintRoleParams, GrantMintRole } from './grant-mint-role.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const OWNER = '0x' + '22'.repeat(20) +const ACCOUNT = '0x' + '33'.repeat(20) +const NOT_THE_OWNER = '0x' + '44'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +/** Calldata built from a fresh Interface — never the SDK's cached one, or this proves nothing. */ +const FRESH = new Interface([ + 'function grantMintRole(address minter)', + 'function isMinter(address minter) view returns (bool)', + 'function owner() view returns (address)', +]) +const expectedData = (minter = ACCOUNT) => FRESH.encodeFunctionData('grantMintRole', [minter]) + +/** The `eth_call`s the op makes, in order, as decoded function names. */ +type Seen = { calls: string[] } +const newSeen = (): Seen => ({ calls: [] }) + +/** + * EVMChain stub for a BurnMintERC677 token owned by `OWNER`. `holdsRole` is the pre-existing + * role state of the account being granted — false is the state that makes this call a real change. + */ +function stubChain({ + holdsRole = false, + owner = OWNER, + callError, + seen = newSeen(), +}: { + holdsRole?: boolean + owner?: string + /** Fails every `eth_call`, standing in for a contract that is not a BurnMintERC677 token. */ + callError?: Error + seen?: Seen +} = {}): EVMChain { + const results: Record = { isMinter: [holdsRole], owner: [owner] } + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + if (callError) return Promise.reject(callError) + const fn = FRESH.getFunction(data.slice(0, 10))!.name + seen.calls.push(fn) + return Promise.resolve(FRESH.encodeFunctionResult(fn, results[fn])) + }, + }, + nextNonce: () => Promise.resolve(0), + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(waitError?: Error, address = OWNER) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => (waitError ? Promise.reject(waitError) : Promise.resolve({ status: 1 })), + }), + } +} + +/** A revert with no data, the shape a call to an undeclared function produces. */ +const missingFunction = () => + makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + +const op = new GrantMintRole() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { tokenAddress: TOKEN, minter: ACCOUNT, sender: OWNER, ...overrides }) +} + +describe('GrantMintRole (cct/evm)', () => { + describe('generate', () => { + it('encodes grantMintRole(address) to the token', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, TOKEN) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, expectedData()) + // the role read comes first: it is also the family check, so it gates the owner read + assert.deepEqual(seen.calls, ['isMinter', 'owner']) + }) + + it('omits from when sender is not supplied, but still probes the token', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen }), { sender: undefined }) + + assert.equal(unsigned.transactions[0]!.from, undefined) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + // no sender to compare, so the owner read is skipped — the family check is not + assert.deepEqual(seen.calls, ['isMinter']) + }) + }) + + describe('validation', () => { + for (const [param, value] of [ + ['tokenAddress', 'not-an-address'], + // a tx to `0x0` hits no code, so it mines as a successful no-op instead of reverting + ['tokenAddress', ZeroAddress], + ['minter', 'not-an-address'], + ['minter', ZeroAddress], + ['sender', 'not-an-address'], + ] as const) { + it(`rejects ${param} = ${String(value)} before any RPC`, async () => { + const seen = newSeen() + await assert.rejects( + () => generate(stubChain({ seen }), { [param]: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'grantMintRole' && + err.context.param === param, + ) + assert.deepEqual(seen.calls, []) + }) + } + }) + + describe('family check', () => { + it('rejects a contract that is not a BurnMintERC677 token', async () => { + // a v2.0.0 CrossChainToken, a token pool, and an EOA all fail the isMinter read + await assert.rejects( + () => generate(stubChain({ callError: missingFunction() })), + (err: unknown) => + err instanceof CCTContractTypeInvalidError && err.context.address === TOKEN, + ) + }) + + it('rejects an unrelated address even with no sender to check', async () => { + // the case an owner gate alone would miss: a token pool declares owner() too + await assert.rejects( + () => generate(stubChain({ callError: missingFunction() }), { sender: undefined }), + (err: unknown) => err instanceof CCTContractTypeInvalidError, + ) + }) + }) + + describe('no-op guard', () => { + it('rejects a grant when the account already holds the mint role', async () => { + // stricter than the chain: the role set is an EnumerableSet, so this would mine as a + // silent no-op rather than revert + const seen = newSeen() + await assert.rejects( + () => generate(stubChain({ holdsRole: true, seen })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'grantMintRole' && + err.context.param === 'minter' && + /already holds the mint role/.test(String(err.context.reason)), + ) + // rejected on the role read alone, before the owner read + assert.deepEqual(seen.calls, ['isMinter']) + }) + + it('rejects the no-op with no sender supplied too', async () => { + await assert.rejects( + () => generate(stubChain({ holdsRole: true }), { sender: undefined }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'minter', + ) + }) + }) + + describe('owner gate', () => { + it('rejects a sender that does not own the token', async () => { + await assert.rejects( + () => generate(stubChain(), { sender: NOT_THE_OWNER }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + /must be the current token owner/.test(String(err.context.reason)), + ) + }) + }) + + describe('execute', () => { + it('submits as the token owner and returns the tx hash', async () => { + const { hash } = await op.execute(stubChain(), { + tokenAddress: TOKEN, + minter: ACCOUNT, + wallet: fakeSigner(), + }) + assert.equal(hash, HASH) + }) + + it('rejects a sender that is not the signing wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + minter: ACCOUNT, + sender: NOT_THE_OWNER, + wallet: fakeSigner(), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a wallet that does not own the token', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + minter: ACCOUNT, + wallet: fakeSigner(undefined, NOT_THE_OWNER), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('surfaces an on-chain revert', async () => { + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'sendTransaction', + data: '0x', + reason: 'OnlyOwner', + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + minter: ACCOUNT, + wallet: fakeSigner(revert), + }), + (err: unknown) => err instanceof CCIPExecTxRevertedError, + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { tokenAddress: TOKEN, minter: ACCOUNT, wallet: {} }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/grant-mint-role.ts b/ccip-sdk/src/cct/evm/token/operations/grant-mint-role.ts new file mode 100644 index 000000000..b31ba7c18 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/grant-mint-role.ts @@ -0,0 +1,80 @@ +/** + * grantMintRole: grants a BurnMintERC677 token's mint role to one account. Owner-gated + * (`onlyOwner`); the owner is the token's mint/burn role admin. + * + * @packageDocumentation + */ + +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 { validateNonZeroAddress } from '../../validate.ts' +import { assertTokenOwner, getErc20Token, readTokenRole } from '../contracts.ts' + +/** Parameters for {@link GrantMintRole}. */ +export type GrantMintRoleParams = { + /** BurnMintERC677 token (v1.5.1 / v1.6.2) whose roles are being changed. */ + tokenAddress: string + /** Account receiving the mint role; must not already hold it. */ + minter: string + /** Current token owner (the role admin); sets `tx.from` for offline / multisig signing. */ + sender?: string +} + +/** Grants the mint role on a BurnMintERC677 token via `grantMintRole`. */ +export class GrantMintRole extends EVMOperation { + readonly name = 'grantMintRole' + + /** + * Validates both addresses before any RPC. Neither may be zero: a tx to `0x0` hits no code, and + * granting a role to `0x0` mines as a no-op nobody can use. + */ + protected override validate({ tokenAddress, minter }: GrantMintRoleParams): void { + validateNonZeroAddress(this.name, 'tokenAddress', tokenAddress) + validateNonZeroAddress(this.name, 'minter', minter) + } + + /** + * Reads the current role state, then — when `sender` is known — confirms it owns the token. + * + * The role read runs first because it is also the family check ({@link readTokenRole}), which + * `owner()` cannot make: a token pool and a v2.0.0 `CrossChainToken` declare `owner()` too. Both + * checks run here rather than in {@link execute}, so the offline / multisig path gets them, and + * a redundant grant is rejected even though the chain would mine it as a silent no-op. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * @throws {@link CCTParamsInvalidError} if `minter` already holds the mint role, or `sender` + * is given and is not the token owner + */ + protected async buildUnsigned( + chain: EVMChain, + { tokenAddress, minter, sender }: GrantMintRoleParams, + ): Promise { + if (await readTokenRole(chain, tokenAddress, 'isMinter', minter)) + throw new CCTParamsInvalidError( + this.name, + 'minter', + `already holds the mint role on ${tokenAddress}; granting it again changes nothing`, + ) + if (sender !== undefined) await assertTokenOwner(this.name, chain, tokenAddress, sender) + + return callTx(tokenAddress, getErc20Token().encodeFunctionData('grantMintRole', [minter])) + } + + /** + * Signs and submits as the token owner, defaulting `sender` to the signing wallet — the only + * address that can satisfy {@link buildUnsigned}'s owner check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} 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, + * or if any other param is invalid (see {@link buildUnsigned}) + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/revoke-burn-role.test.ts b/ccip-sdk/src/cct/evm/token/operations/revoke-burn-role.test.ts new file mode 100644 index 000000000..72f41574c --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/revoke-burn-role.test.ts @@ -0,0 +1,260 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../../../errors.ts' +import { type RevokeBurnRoleParams, RevokeBurnRole } from './revoke-burn-role.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const OWNER = '0x' + '22'.repeat(20) +const ACCOUNT = '0x' + '33'.repeat(20) +const NOT_THE_OWNER = '0x' + '44'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +/** Calldata built from a fresh Interface — never the SDK's cached one, or this proves nothing. */ +const FRESH = new Interface([ + 'function revokeBurnRole(address burner)', + 'function isBurner(address burner) view returns (bool)', + 'function owner() view returns (address)', +]) +const expectedData = (burner = ACCOUNT) => FRESH.encodeFunctionData('revokeBurnRole', [burner]) + +/** The `eth_call`s the op makes, in order, as decoded function names. */ +type Seen = { calls: string[] } +const newSeen = (): Seen => ({ calls: [] }) + +/** + * EVMChain stub for a BurnMintERC677 token owned by `OWNER`. `holdsRole` is the pre-existing + * role state of the account being revokeed — true is the state that makes this call a real change. + */ +function stubChain({ + holdsRole = true, + owner = OWNER, + callError, + seen = newSeen(), +}: { + holdsRole?: boolean + owner?: string + /** Fails every `eth_call`, standing in for a contract that is not a BurnMintERC677 token. */ + callError?: Error + seen?: Seen +} = {}): EVMChain { + const results: Record = { isBurner: [holdsRole], owner: [owner] } + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + if (callError) return Promise.reject(callError) + const fn = FRESH.getFunction(data.slice(0, 10))!.name + seen.calls.push(fn) + return Promise.resolve(FRESH.encodeFunctionResult(fn, results[fn])) + }, + }, + nextNonce: () => Promise.resolve(0), + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(waitError?: Error, address = OWNER) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => (waitError ? Promise.reject(waitError) : Promise.resolve({ status: 1 })), + }), + } +} + +/** A revert with no data, the shape a call to an undeclared function produces. */ +const missingFunction = () => + makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + +const op = new RevokeBurnRole() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { tokenAddress: TOKEN, burner: ACCOUNT, sender: OWNER, ...overrides }) +} + +describe('RevokeBurnRole (cct/evm)', () => { + describe('generate', () => { + it('encodes revokeBurnRole(address) to the token', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, TOKEN) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, expectedData()) + // the role read comes first: it is also the family check, so it gates the owner read + assert.deepEqual(seen.calls, ['isBurner', 'owner']) + }) + + it('omits from when sender is not supplied, but still probes the token', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen }), { sender: undefined }) + + assert.equal(unsigned.transactions[0]!.from, undefined) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + // no sender to compare, so the owner read is skipped — the family check is not + assert.deepEqual(seen.calls, ['isBurner']) + }) + }) + + describe('validation', () => { + for (const [param, value] of [ + ['tokenAddress', 'not-an-address'], + // a tx to `0x0` hits no code, so it mines as a successful no-op instead of reverting + ['tokenAddress', ZeroAddress], + ['burner', 'not-an-address'], + ['burner', ZeroAddress], + ['sender', 'not-an-address'], + ] as const) { + it(`rejects ${param} = ${String(value)} before any RPC`, async () => { + const seen = newSeen() + await assert.rejects( + () => generate(stubChain({ seen }), { [param]: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'revokeBurnRole' && + err.context.param === param, + ) + assert.deepEqual(seen.calls, []) + }) + } + }) + + describe('family check', () => { + it('rejects a contract that is not a BurnMintERC677 token', async () => { + // a v2.0.0 CrossChainToken, a token pool, and an EOA all fail the isBurner read + await assert.rejects( + () => generate(stubChain({ callError: missingFunction() })), + (err: unknown) => + err instanceof CCTContractTypeInvalidError && err.context.address === TOKEN, + ) + }) + + it('rejects an unrelated address even with no sender to check', async () => { + // the case an owner gate alone would miss: a token pool declares owner() too + await assert.rejects( + () => generate(stubChain({ callError: missingFunction() }), { sender: undefined }), + (err: unknown) => err instanceof CCTContractTypeInvalidError, + ) + }) + }) + + describe('no-op guard', () => { + it('rejects a revoke when the account does not hold the burn role', async () => { + // stricter than the chain: the role set is an EnumerableSet, so this would mine as a + // silent no-op rather than revert + const seen = newSeen() + await assert.rejects( + () => generate(stubChain({ holdsRole: false, seen })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'revokeBurnRole' && + err.context.param === 'burner' && + /does not hold the burn role/.test(String(err.context.reason)), + ) + // rejected on the role read alone, before the owner read + assert.deepEqual(seen.calls, ['isBurner']) + }) + + it('rejects the no-op with no sender supplied too', async () => { + await assert.rejects( + () => generate(stubChain({ holdsRole: false }), { sender: undefined }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'burner', + ) + }) + }) + + describe('owner gate', () => { + it('rejects a sender that does not own the token', async () => { + await assert.rejects( + () => generate(stubChain(), { sender: NOT_THE_OWNER }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + /must be the current token owner/.test(String(err.context.reason)), + ) + }) + }) + + describe('execute', () => { + it('submits as the token owner and returns the tx hash', async () => { + const { hash } = await op.execute(stubChain(), { + tokenAddress: TOKEN, + burner: ACCOUNT, + wallet: fakeSigner(), + }) + assert.equal(hash, HASH) + }) + + it('rejects a sender that is not the signing wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + burner: ACCOUNT, + sender: NOT_THE_OWNER, + wallet: fakeSigner(), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a wallet that does not own the token', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + burner: ACCOUNT, + wallet: fakeSigner(undefined, NOT_THE_OWNER), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('surfaces an on-chain revert', async () => { + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'sendTransaction', + data: '0x', + reason: 'OnlyOwner', + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + burner: ACCOUNT, + wallet: fakeSigner(revert), + }), + (err: unknown) => err instanceof CCIPExecTxRevertedError, + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { tokenAddress: TOKEN, burner: ACCOUNT, wallet: {} }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/revoke-burn-role.ts b/ccip-sdk/src/cct/evm/token/operations/revoke-burn-role.ts new file mode 100644 index 000000000..4c981693d --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/revoke-burn-role.ts @@ -0,0 +1,80 @@ +/** + * revokeBurnRole: removes a BurnMintERC677 token's burn role from one account. Owner-gated + * (`onlyOwner`); the owner is the token's mint/burn role admin. + * + * @packageDocumentation + */ + +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 { validateNonZeroAddress } from '../../validate.ts' +import { assertTokenOwner, getErc20Token, readTokenRole } from '../contracts.ts' + +/** Parameters for {@link RevokeBurnRole}. */ +export type RevokeBurnRoleParams = { + /** BurnMintERC677 token (v1.5.1 / v1.6.2) whose roles are being changed. */ + tokenAddress: string + /** Account losing the burn role; must currently hold it. */ + burner: string + /** Current token owner (the role admin); sets `tx.from` for offline / multisig signing. */ + sender?: string +} + +/** Removes the burn role from an account on a BurnMintERC677 token via `revokeBurnRole`. */ +export class RevokeBurnRole extends EVMOperation { + readonly name = 'revokeBurnRole' + + /** + * Validates both addresses before any RPC. Neither may be zero: a tx to `0x0` hits no code, and + * revoking a role from `0x0` mines as a no-op. + */ + protected override validate({ tokenAddress, burner }: RevokeBurnRoleParams): void { + validateNonZeroAddress(this.name, 'tokenAddress', tokenAddress) + validateNonZeroAddress(this.name, 'burner', burner) + } + + /** + * Reads the current role state, then — when `sender` is known — confirms it owns the token. + * + * The role read runs first because it is also the family check ({@link readTokenRole}), which + * `owner()` cannot make: a token pool and a v2.0.0 `CrossChainToken` declare `owner()` too. Both + * checks run here rather than in {@link execute}, so the offline / multisig path gets them, and + * revoking a role never held is rejected even though the chain would mine it as a silent no-op. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * @throws {@link CCTParamsInvalidError} if `burner` does not hold the burn role, or `sender` + * is given and is not the token owner + */ + protected async buildUnsigned( + chain: EVMChain, + { tokenAddress, burner, sender }: RevokeBurnRoleParams, + ): Promise { + if (!(await readTokenRole(chain, tokenAddress, 'isBurner', burner))) + throw new CCTParamsInvalidError( + this.name, + 'burner', + `does not hold the burn role on ${tokenAddress}; revoking it changes nothing`, + ) + if (sender !== undefined) await assertTokenOwner(this.name, chain, tokenAddress, sender) + + return callTx(tokenAddress, getErc20Token().encodeFunctionData('revokeBurnRole', [burner])) + } + + /** + * Signs and submits as the token owner, defaulting `sender` to the signing wallet — the only + * address that can satisfy {@link buildUnsigned}'s owner check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} 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, + * or if any other param is invalid (see {@link buildUnsigned}) + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +} diff --git a/ccip-sdk/src/cct/evm/token/operations/revoke-mint-role.test.ts b/ccip-sdk/src/cct/evm/token/operations/revoke-mint-role.test.ts new file mode 100644 index 000000000..80f72e9e4 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/revoke-mint-role.test.ts @@ -0,0 +1,260 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { Interface, ZeroAddress, makeError } from 'ethers' + +import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts' +import type { EVMChain } from '../../../../evm/index.ts' +import { ChainFamily } from '../../../../networks.ts' +import { CCTContractTypeInvalidError, CCTParamsInvalidError } from '../../../errors.ts' +import { type RevokeMintRoleParams, RevokeMintRole } from './revoke-mint-role.ts' + +const TOKEN = '0x' + '11'.repeat(20) +const OWNER = '0x' + '22'.repeat(20) +const ACCOUNT = '0x' + '33'.repeat(20) +const NOT_THE_OWNER = '0x' + '44'.repeat(20) +const HASH = '0x' + 'ab'.repeat(32) + +/** Calldata built from a fresh Interface — never the SDK's cached one, or this proves nothing. */ +const FRESH = new Interface([ + 'function revokeMintRole(address minter)', + 'function isMinter(address minter) view returns (bool)', + 'function owner() view returns (address)', +]) +const expectedData = (minter = ACCOUNT) => FRESH.encodeFunctionData('revokeMintRole', [minter]) + +/** The `eth_call`s the op makes, in order, as decoded function names. */ +type Seen = { calls: string[] } +const newSeen = (): Seen => ({ calls: [] }) + +/** + * EVMChain stub for a BurnMintERC677 token owned by `OWNER`. `holdsRole` is the pre-existing + * role state of the account being revokeed — true is the state that makes this call a real change. + */ +function stubChain({ + holdsRole = true, + owner = OWNER, + callError, + seen = newSeen(), +}: { + holdsRole?: boolean + owner?: string + /** Fails every `eth_call`, standing in for a contract that is not a BurnMintERC677 token. */ + callError?: Error + seen?: Seen +} = {}): EVMChain { + const results: Record = { isMinter: [holdsRole], owner: [owner] } + return { + logger: { debug() {}, info() {}, warn() {}, error() {} }, + provider: { + call: ({ data }: { data: string }) => { + if (callError) return Promise.reject(callError) + const fn = FRESH.getFunction(data.slice(0, 10))!.name + seen.calls.push(fn) + return Promise.resolve(FRESH.encodeFunctionResult(fn, results[fn])) + }, + }, + nextNonce: () => Promise.resolve(0), + rollbackNonce: () => {}, + } as unknown as EVMChain +} + +function fakeSigner(waitError?: Error, address = OWNER) { + return { + signTransaction: () => Promise.resolve('0x'), + getAddress: () => Promise.resolve(address), + populateTransaction: (tx: unknown) => Promise.resolve({ ...(tx as object) }), + sendTransaction: () => + Promise.resolve({ + hash: HASH, + wait: () => (waitError ? Promise.reject(waitError) : Promise.resolve({ status: 1 })), + }), + } +} + +/** A revert with no data, the shape a call to an undeclared function produces. */ +const missingFunction = () => + makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'call', + data: '0x', + reason: null, + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + +const op = new RevokeMintRole() + +function generate(chain: EVMChain, overrides: Partial = {}) { + return op.generate(chain, { tokenAddress: TOKEN, minter: ACCOUNT, sender: OWNER, ...overrides }) +} + +describe('RevokeMintRole (cct/evm)', () => { + describe('generate', () => { + it('encodes revokeMintRole(address) to the token', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen })) + const tx = unsigned.transactions[0]! + + assert.equal(unsigned.family, ChainFamily.EVM) + assert.equal(unsigned.transactions.length, 1) + assert.equal(tx.to, TOKEN) + assert.equal(tx.from, OWNER) + assert.equal(tx.data, expectedData()) + // the role read comes first: it is also the family check, so it gates the owner read + assert.deepEqual(seen.calls, ['isMinter', 'owner']) + }) + + it('omits from when sender is not supplied, but still probes the token', async () => { + const seen = newSeen() + const unsigned = await generate(stubChain({ seen }), { sender: undefined }) + + assert.equal(unsigned.transactions[0]!.from, undefined) + assert.equal(unsigned.transactions[0]!.data, expectedData()) + // no sender to compare, so the owner read is skipped — the family check is not + assert.deepEqual(seen.calls, ['isMinter']) + }) + }) + + describe('validation', () => { + for (const [param, value] of [ + ['tokenAddress', 'not-an-address'], + // a tx to `0x0` hits no code, so it mines as a successful no-op instead of reverting + ['tokenAddress', ZeroAddress], + ['minter', 'not-an-address'], + ['minter', ZeroAddress], + ['sender', 'not-an-address'], + ] as const) { + it(`rejects ${param} = ${String(value)} before any RPC`, async () => { + const seen = newSeen() + await assert.rejects( + () => generate(stubChain({ seen }), { [param]: value }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'revokeMintRole' && + err.context.param === param, + ) + assert.deepEqual(seen.calls, []) + }) + } + }) + + describe('family check', () => { + it('rejects a contract that is not a BurnMintERC677 token', async () => { + // a v2.0.0 CrossChainToken, a token pool, and an EOA all fail the isMinter read + await assert.rejects( + () => generate(stubChain({ callError: missingFunction() })), + (err: unknown) => + err instanceof CCTContractTypeInvalidError && err.context.address === TOKEN, + ) + }) + + it('rejects an unrelated address even with no sender to check', async () => { + // the case an owner gate alone would miss: a token pool declares owner() too + await assert.rejects( + () => generate(stubChain({ callError: missingFunction() }), { sender: undefined }), + (err: unknown) => err instanceof CCTContractTypeInvalidError, + ) + }) + }) + + describe('no-op guard', () => { + it('rejects a revoke when the account does not hold the mint role', async () => { + // stricter than the chain: the role set is an EnumerableSet, so this would mine as a + // silent no-op rather than revert + const seen = newSeen() + await assert.rejects( + () => generate(stubChain({ holdsRole: false, seen })), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.operation === 'revokeMintRole' && + err.context.param === 'minter' && + /does not hold the mint role/.test(String(err.context.reason)), + ) + // rejected on the role read alone, before the owner read + assert.deepEqual(seen.calls, ['isMinter']) + }) + + it('rejects the no-op with no sender supplied too', async () => { + await assert.rejects( + () => generate(stubChain({ holdsRole: false }), { sender: undefined }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'minter', + ) + }) + }) + + describe('owner gate', () => { + it('rejects a sender that does not own the token', async () => { + await assert.rejects( + () => generate(stubChain(), { sender: NOT_THE_OWNER }), + (err: unknown) => + err instanceof CCTParamsInvalidError && + err.context.param === 'sender' && + /must be the current token owner/.test(String(err.context.reason)), + ) + }) + }) + + describe('execute', () => { + it('submits as the token owner and returns the tx hash', async () => { + const { hash } = await op.execute(stubChain(), { + tokenAddress: TOKEN, + minter: ACCOUNT, + wallet: fakeSigner(), + }) + assert.equal(hash, HASH) + }) + + it('rejects a sender that is not the signing wallet', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + minter: ACCOUNT, + sender: NOT_THE_OWNER, + wallet: fakeSigner(), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('rejects a wallet that does not own the token', async () => { + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + minter: ACCOUNT, + wallet: fakeSigner(undefined, NOT_THE_OWNER), + }), + (err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'sender', + ) + }) + + it('surfaces an on-chain revert', async () => { + const revert = makeError('execution reverted', 'CALL_EXCEPTION', { + action: 'sendTransaction', + data: '0x', + reason: 'OnlyOwner', + transaction: { to: TOKEN, data: '0x' }, + invocation: null, + revert: null, + }) + await assert.rejects( + () => + op.execute(stubChain(), { + tokenAddress: TOKEN, + minter: ACCOUNT, + wallet: fakeSigner(revert), + }), + (err: unknown) => err instanceof CCIPExecTxRevertedError, + ) + }) + + it('rejects a non-signer wallet', async () => { + await assert.rejects( + () => op.execute(stubChain(), { tokenAddress: TOKEN, minter: ACCOUNT, wallet: {} }), + (err: unknown) => err instanceof CCIPWalletInvalidError, + ) + }) + }) +}) diff --git a/ccip-sdk/src/cct/evm/token/operations/revoke-mint-role.ts b/ccip-sdk/src/cct/evm/token/operations/revoke-mint-role.ts new file mode 100644 index 000000000..152c14398 --- /dev/null +++ b/ccip-sdk/src/cct/evm/token/operations/revoke-mint-role.ts @@ -0,0 +1,80 @@ +/** + * revokeMintRole: removes a BurnMintERC677 token's mint role from one account. Owner-gated + * (`onlyOwner`); the owner is the token's mint/burn role admin. + * + * @packageDocumentation + */ + +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 { validateNonZeroAddress } from '../../validate.ts' +import { assertTokenOwner, getErc20Token, readTokenRole } from '../contracts.ts' + +/** Parameters for {@link RevokeMintRole}. */ +export type RevokeMintRoleParams = { + /** BurnMintERC677 token (v1.5.1 / v1.6.2) whose roles are being changed. */ + tokenAddress: string + /** Account losing the mint role; must currently hold it. */ + minter: string + /** Current token owner (the role admin); sets `tx.from` for offline / multisig signing. */ + sender?: string +} + +/** Removes the mint role from an account on a BurnMintERC677 token via `revokeMintRole`. */ +export class RevokeMintRole extends EVMOperation { + readonly name = 'revokeMintRole' + + /** + * Validates both addresses before any RPC. Neither may be zero: a tx to `0x0` hits no code, and + * revoking a role from `0x0` mines as a no-op. + */ + protected override validate({ tokenAddress, minter }: RevokeMintRoleParams): void { + validateNonZeroAddress(this.name, 'tokenAddress', tokenAddress) + validateNonZeroAddress(this.name, 'minter', minter) + } + + /** + * Reads the current role state, then — when `sender` is known — confirms it owns the token. + * + * The role read runs first because it is also the family check ({@link readTokenRole}), which + * `owner()` cannot make: a token pool and a v2.0.0 `CrossChainToken` declare `owner()` too. Both + * checks run here rather than in {@link execute}, so the offline / multisig path gets them, and + * revoking a role never held is rejected even though the chain would mine it as a silent no-op. + * @throws {@link CCTContractTypeInvalidError} if `tokenAddress` is not a BurnMintERC677 token + * @throws {@link CCTParamsInvalidError} if `minter` does not hold the mint role, or `sender` + * is given and is not the token owner + */ + protected async buildUnsigned( + chain: EVMChain, + { tokenAddress, minter, sender }: RevokeMintRoleParams, + ): Promise { + if (!(await readTokenRole(chain, tokenAddress, 'isMinter', minter))) + throw new CCTParamsInvalidError( + this.name, + 'minter', + `does not hold the mint role on ${tokenAddress}; revoking it changes nothing`, + ) + if (sender !== undefined) await assertTokenOwner(this.name, chain, tokenAddress, sender) + + return callTx(tokenAddress, getErc20Token().encodeFunctionData('revokeMintRole', [minter])) + } + + /** + * Signs and submits as the token owner, defaulting `sender` to the signing wallet — the only + * address that can satisfy {@link buildUnsigned}'s owner check for a broadcast tx. See + * {@link EVMOperation.resolveWalletSender} 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, + * or if any other param is invalid (see {@link buildUnsigned}) + */ + override async execute( + chain: EVMChain, + params: EVMExecuteParams, + ): Promise { + const sender = await this.resolveWalletSender(params.wallet, params.sender) + return super.execute(chain, { ...params, sender }) + } +}