diff --git a/ccip-sdk/src/cct/solana/index.test.ts b/ccip-sdk/src/cct/solana/index.test.ts index ddbb6de31..d8a1f7316 100644 --- a/ccip-sdk/src/cct/solana/index.test.ts +++ b/ccip-sdk/src/cct/solana/index.test.ts @@ -62,10 +62,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 33c4db74a..cb32d3f51 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)