Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
39983bd
feat: add transfer pool ownership op solana
mervin-link Aug 17, 2026
df46d8d
feat: add accept pool ownership op solana
mervin-link Aug 17, 2026
d296442
feat: add transfer authority op solana
mervin-link Aug 17, 2026
5547e60
fix: update export barrel
mervin-link Aug 18, 2026
126ec98
feat: add mint tokens op solana
mervin-link Aug 18, 2026
e46e1ef
fix: merge conflicts
mervin-link Aug 21, 2026
0eda9e2
Merge branch 'feat/DAPP-11191-transfer-authority' into feat/DAPP-1119…
mervin-link Aug 21, 2026
2120d95
fix: address comments
mervin-link Aug 24, 2026
7aa7ba8
fix: merge conflicts
mervin-link Aug 24, 2026
cbeffaf
fix: address comments
mervin-link Aug 24, 2026
2c3c86f
feat: add set can accept liquidity op solana
mervin-link Aug 24, 2026
24b7262
fix: add lock release token pool idl
mervin-link Aug 24, 2026
759f9c4
feat: add set rebalancer op solana
mervin-link Aug 24, 2026
61f2dd1
fix: update tsdoc
mervin-link Aug 25, 2026
f5de283
feat: add approve token op solana
mervin-link Aug 25, 2026
7db0b40
feat: add provider liquidity op solana
mervin-link Aug 25, 2026
95aaa54
fix: merge conflicts
mervin-link Aug 27, 2026
b56a872
fix: address comments
mervin-link Aug 27, 2026
da3d0a8
fix: revert unrelated changes
mervin-link Aug 27, 2026
af16159
Merge branch 'feat/DAPP-11229-approve-token' into feat/DAPP-11223-pro…
mervin-link Aug 27, 2026
37166c7
fix: add preflight checks
mervin-link Aug 27, 2026
2b09e87
fix: update tsdoc
mervin-link Aug 27, 2026
8ecbfc7
fix: extract validate pool liquidity config
mervin-link Aug 27, 2026
072d8fe
feat: add withdraw liquidity op solana
mervin-link Aug 26, 2026
c5e0bac
fix: merge conflicts
mervin-link Aug 27, 2026
9ba8621
fix: add preflight checks
mervin-link Aug 27, 2026
d89ba8a
feat: add update metadata authority op solana
mervin-link Aug 27, 2026
6d8b1a0
Merge branch 'cct-sdk' into feat/DAPP-11223-provide-liquidity
mervin-link Aug 28, 2026
40997e9
fix: lint errors
mervin-link Aug 28, 2026
48d2bc3
Merge branch 'feat/DAPP-11223-provide-liquidity' into feat/DAPP-11224…
mervin-link Aug 28, 2026
8ce4a3d
fix: lint errors
mervin-link Aug 28, 2026
8a7c987
fix: merge conflicts
mervin-link Aug 28, 2026
7eec581
fix: address comments
mervin-link Aug 31, 2026
a53f911
fix: refactor unit tests
mervin-link Aug 31, 2026
53989ad
Merge branch 'cct-sdk' into feat/DAPP-11230-update-metadata-authority
mervin-link Sep 2, 2026
6e657e3
Merge branch 'feat/DAPP-11230-update-metadata-authority' into fix/sol…
mervin-link Sep 2, 2026
e034631
fix: refactor export
mervin-link Sep 2, 2026
c68e439
feat: add owner override pending admint op solana
mervin-link Sep 2, 2026
992fc16
fix: update tsdoc and add preflight check
mervin-link Sep 2, 2026
4bb5493
fix: update tsdoc @example
mervin-link Sep 3, 2026
8a8bf60
feat: add includeApproval option in provide liquidity
mervin-link Sep 4, 2026
a9bde75
feat: add createRecipientATA option to mint tokens op
mervin-link Sep 4, 2026
85f9058
feat: add get token info op solana
mervin-link Sep 7, 2026
45b968d
Merge branch 'cct-sdk' into feat/DAPP-11371-get-token-info
mervin-link Sep 8, 2026
5549095
fix: address comments
mervin-link Sep 8, 2026
5f41774
Merge branch 'cct-sdk' into feat/DAPP-11371-get-token-info
mervin-link Sep 8, 2026
f0639c5
fix: package lock file - remove stale axios version
mervin-link Sep 8, 2026
f2357e1
feat(cct-sdk):Add createPoolSignerATA option to deploy token pool (#412)
mervin-link Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ccip-sdk/src/cct/solana/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -779,6 +780,7 @@ describe('SolanaTokenManager (cct/solana)', () => {
remoteChainSelector,
}),
],
['getTokenInfo', () => cct.getTokenInfo({ tokenAddress: mint })],
[
'getTokenPoolState',
() =>
Expand Down
73 changes: 68 additions & 5 deletions ccip-sdk/src/cct/solana/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,11 @@ import {
type GenerateSetTokenAuthorityResult,
type GenerateUpdateMetadataAuthorityParams,
type GenerateUpdateMetadataAuthorityResult,
type GetTokenInfoParams,
type GetTokenInfoResult,
ApproveToken,
CreateTokenAccount,
GetTokenInfo,
MintTokens,
SetTokenAuthority,
UpdateMetadataAuthority,
Expand All @@ -190,6 +193,7 @@ export class SolanaTokenManager extends TokenManager<typeof ChainFamily.Solana>
// 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()
Expand Down Expand Up @@ -818,12 +822,25 @@ export class SolanaTokenManager extends TokenManager<typeof ChainFamily.Solana>
* @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.
*
Expand All @@ -836,6 +853,7 @@ export class SolanaTokenManager extends TokenManager<typeof ChainFamily.Solana>
* payer,
* authority,
* allowlist: [allowedSender],
* createPoolSignerATA: true,
* })
* ```
*/
Expand All @@ -851,11 +869,25 @@ export class SolanaTokenManager extends TokenManager<typeof ChainFamily.Solana>
* @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.
Expand All @@ -867,6 +899,7 @@ export class SolanaTokenManager extends TokenManager<typeof ChainFamily.Solana>
* await cct.deployTokenPool({
* tokenAddress: mint,
* poolType: 'burn-mint',
* createPoolSignerATA: true,
* wallet,
* })
* ```
Expand Down Expand Up @@ -2230,6 +2263,36 @@ export class SolanaTokenManager extends TokenManager<typeof ChainFamily.Solana>
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<GetTokenInfoResult> {
return this.#getTokenInfo.query(this.chain, opts)
}

/**
* Reads all, or one selected, Solana token pool remote-chain configurations.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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' })

Expand All @@ -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' }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand All @@ -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
}
Expand All @@ -55,6 +67,7 @@ type ParsedDeployTokenPoolParams = {
payer: PublicKey
authority: PublicKey
allowlist: PublicKey[]
createPoolSignerATA: boolean
}

/** Unsigned Solana token pool deploy result plus derived pool PDAs. */
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
}
}

Expand All @@ -107,7 +127,7 @@ export class DeployTokenPool extends SolanaOperation<
chain: SolanaChain,
opts: ParsedDeployTokenPoolParams,
): Promise<GenerateDeployTokenPoolResult> {
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)
Expand All @@ -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
Expand Down
86 changes: 86 additions & 0 deletions ccip-sdk/src/cct/solana/token/operations/get-token-info.test.ts
Original file line number Diff line number Diff line change
@@ -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' }),
)
})
})
})
Loading
Loading