diff --git a/forester/tests/e2e_test.rs b/forester/tests/e2e_test.rs index 24f418489e..afca1dc78f 100644 --- a/forester/tests/e2e_test.rs +++ b/forester/tests/e2e_test.rs @@ -389,11 +389,15 @@ async fn e2e_test() { // Create compressible token account with 0 epochs rent (instantly compressible) // This account is picked up by the bootstrapping process + // First create a mint on-chain (required for token account creation) + let compressible_mint_keypair = Keypair::new(); + let compressible_mint = + create_mint_helper_with_keypair(&mut rpc, &payer, &compressible_mint_keypair).await; let compressible_account_bootstrap = create_compressible_token_account( &mut rpc, CreateCompressibleTokenAccountInputs { owner: Keypair::new().pubkey(), - mint: Pubkey::new_unique(), + mint: compressible_mint, num_prepaid_epochs: 0, payer: &payer, token_account_keypair: None, @@ -439,11 +443,16 @@ async fn e2e_test() { // Create 2nd compressible token account with 0 epochs rent (instantly compressible) // This account is picked up by the subscriber + // First create a mint on-chain (required for token account creation) + let compressible_mint_keypair_subscriber = Keypair::new(); + let compressible_mint_subscriber = + create_mint_helper_with_keypair(&mut rpc, &payer, &compressible_mint_keypair_subscriber) + .await; let compressible_account_subscriber = create_compressible_token_account( &mut rpc, CreateCompressibleTokenAccountInputs { owner: Keypair::new().pubkey(), - mint: Pubkey::new_unique(), + mint: compressible_mint_subscriber, num_prepaid_epochs: 0, payer: &payer, token_account_keypair: None, diff --git a/forester/tests/test_compressible_ctoken.rs b/forester/tests/test_compressible_ctoken.rs index bde04205de..3342fb5cb8 100644 --- a/forester/tests/test_compressible_ctoken.rs +++ b/forester/tests/test_compressible_ctoken.rs @@ -8,10 +8,13 @@ use forester_utils::{ rpc_pool::{SolanaRpcPool, SolanaRpcPoolBuilder}, }; use light_client::{ + indexer::{AddressWithTree, Indexer}, local_test_validator::{spawn_validator, LightValidatorConfig}, rpc::{LightClient, LightClientConfig, Rpc}, }; -use light_compressed_token_sdk::compressed_token::create_compressed_mint; +use light_compressed_token_sdk::compressed_token::create_compressed_mint::{ + derive_mint_compressed_address, find_mint_address, +}; use light_registry::{ protocol_config::state::ProtocolConfigPda, sdk::{ @@ -24,11 +27,77 @@ use light_registry::{ use light_test_utils::actions::legacy::{ create_compressible_token_account, CreateCompressibleTokenAccountInputs, }; +use light_token::instruction::{CreateMint, CreateMintParams}; use light_token_interface::state::TokenDataVersion; use serial_test::serial; use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction}; use tokio::{sync::oneshot, time::sleep}; +/// Helper to create a decompressed mint (auto-decompresses on creation). +/// Returns (mint_pda, mint_seed). +async fn create_decompressed_mint( + rpc: &mut LightClient, + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, +) -> (Pubkey, Keypair) { + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + // Derive compression address + let compression_address = + derive_mint_compressed_address(&mint_seed.pubkey(), &address_tree.tree); + + let (mint_pda, bump) = find_mint_address(&mint_seed.pubkey()); + + // Get validity proof for the address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build params - rent_payment = 2 is the minimum required by the program + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint: mint_pda, + bump, + freeze_authority: None, + extensions: None, + rent_payment: 2, // Minimum required epochs of rent prepayment + write_top_up: 0, + }; + + // Create instruction + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + // Send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .expect("CreateMint should succeed"); + + (mint_pda, mint_seed) +} + /// Context returned from forester registration containing everything needed for compression testing struct ForesterContext { forester_keypair: Keypair, @@ -231,13 +300,8 @@ async fn test_compressible_ctoken_compression() { }); sleep(Duration::from_secs(2)).await; - // Create mint - let mint_seed = Keypair::new(); - let address_tree = rpc.get_address_tree_v2().tree; - let mint = Pubkey::from(create_compressed_mint::derive_mint_compressed_address( - &mint_seed.pubkey(), - &address_tree, - )); + // Create mint (must exist on-chain before creating token accounts) + let (mint, _mint_seed) = create_decompressed_mint(&mut rpc, &payer, payer.pubkey(), 9).await; // Create first account with 2 epochs rent let owner_keypair = Keypair::new(); let token_account_pubkey = create_compressible_token_account( @@ -404,13 +468,8 @@ async fn test_compressible_ctoken_bootstrap() { }) .count(); - // Create mint - let mint_seed = Keypair::new(); - let address_tree = rpc.get_address_tree_v2().tree; - let mint = Pubkey::from(create_compressed_mint::derive_mint_compressed_address( - &mint_seed.pubkey(), - &address_tree, - )); + // Create mint (must exist on-chain before creating token accounts) + let (mint, _mint_seed) = create_decompressed_mint(&mut rpc, &payer, payer.pubkey(), 9).await; // Create 3 compressible token accounts BEFORE bootstrap runs let mut created_pubkeys = vec![]; diff --git a/js/compressed-token/src/v3/actions/decompress-mint.ts b/js/compressed-token/src/v3/actions/decompress-mint.ts new file mode 100644 index 0000000000..a23f25afb9 --- /dev/null +++ b/js/compressed-token/src/v3/actions/decompress-mint.ts @@ -0,0 +1,113 @@ +import { + ComputeBudgetProgram, + ConfirmOptions, + PublicKey, + Signer, + TransactionSignature, +} from '@solana/web3.js'; +import { + Rpc, + buildAndSignTx, + sendAndConfirmTx, + DerivationMode, + bn, + CTOKEN_PROGRAM_ID, + assertBetaEnabled, +} from '@lightprotocol/stateless.js'; +import { createDecompressMintInstruction } from '../instructions/decompress-mint'; +import { getMintInterface } from '../get-mint-interface'; + +export interface DecompressMintParams { + /** Number of epochs to prepay rent (minimum 2, default: 16 for ~24 hours) */ + rentPayment?: number; + /** Per-write top-up in lamports (default: 766 for ~2 epochs) */ + writeTopUp?: number; + /** Compressible config account (default: LIGHT_TOKEN_CONFIG) */ + configAccount?: PublicKey; + /** Rent sponsor PDA (default: LIGHT_TOKEN_RENT_SPONSOR) */ + rentSponsor?: PublicKey; +} + +/** + * Decompress a compressed mint to create the CMint Solana account. + * + * This makes the mint available on-chain, which is required before creating + * CToken associated token accounts. DecompressMint is **permissionless** - + * any account can call it. + * + * @param rpc - RPC connection + * @param payer - Fee payer (signer) + * @param mint - Mint address + * @param authority - Authority signer (can be any account, required for MintAction) + * @param params - Optional decompression parameters + * @param confirmOptions - Optional confirm options + * @returns Transaction signature + */ +export async function decompressMint( + rpc: Rpc, + payer: Signer, + mint: PublicKey, + authority?: Signer, + params?: DecompressMintParams, + confirmOptions?: ConfirmOptions, +): Promise { + assertBetaEnabled(); + + // Use payer as authority if not provided (decompressMint is permissionless) + const effectiveAuthority = authority ?? payer; + + const mintInterface = await getMintInterface( + rpc, + mint, + confirmOptions?.commitment, + CTOKEN_PROGRAM_ID, + ); + + if (!mintInterface.merkleContext) { + throw new Error('Mint does not have MerkleContext'); + } + + // Check if already decompressed + if (mintInterface.mintContext?.cmintDecompressed) { + throw new Error('Mint is already decompressed'); + } + + const validityProof = await rpc.getValidityProofV2( + [ + { + hash: bn(mintInterface.merkleContext.hash), + leafIndex: mintInterface.merkleContext.leafIndex, + treeInfo: mintInterface.merkleContext.treeInfo, + proveByIndex: mintInterface.merkleContext.proveByIndex, + }, + ], + [], + DerivationMode.compressible, + ); + + const ix = createDecompressMintInstruction({ + mintInterface, + authority: effectiveAuthority.publicKey, + payer: payer.publicKey, + validityProof, + rentPayment: params?.rentPayment, + writeTopUp: params?.writeTopUp, + configAccount: params?.configAccount, + rentSponsor: params?.rentSponsor, + }); + + const additionalSigners: Signer[] = []; + if (authority && !effectiveAuthority.publicKey.equals(payer.publicKey)) { + additionalSigners.push(effectiveAuthority); + } + + const { blockhash } = await rpc.getLatestBlockhash(); + const tx = buildAndSignTx( + [ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), ix], + payer, + blockhash, + additionalSigners, + ); + + return await sendAndConfirmTx(rpc, tx, confirmOptions); +} diff --git a/js/compressed-token/src/v3/actions/index.ts b/js/compressed-token/src/v3/actions/index.ts index 57c4ff557d..84cc9f6d28 100644 --- a/js/compressed-token/src/v3/actions/index.ts +++ b/js/compressed-token/src/v3/actions/index.ts @@ -1,6 +1,7 @@ export * from './create-mint-interface'; export * from './update-mint'; export * from './update-metadata'; +export * from './decompress-mint'; export * from './create-associated-ctoken'; export * from './create-ata-interface'; export * from './mint-to'; diff --git a/js/compressed-token/src/v3/actions/mint-to-interface.ts b/js/compressed-token/src/v3/actions/mint-to-interface.ts index b16531ee7b..43eead0a6c 100644 --- a/js/compressed-token/src/v3/actions/mint-to-interface.ts +++ b/js/compressed-token/src/v3/actions/mint-to-interface.ts @@ -9,8 +9,6 @@ import { Rpc, buildAndSignTx, sendAndConfirmTx, - DerivationMode, - bn, assertBetaEnabled, } from '@lightprotocol/stateless.js'; import { createMintToInterfaceInstruction } from '../instructions/mint-to-interface'; @@ -18,20 +16,22 @@ import { getMintInterface } from '../get-mint-interface'; /** * Mint tokens to a decompressed/onchain token account. - * Works with SPL, Token-2022, and compressed token (c-token) mints. + * Works with SPL, Token-2022, and CToken mints. * * This function ONLY mints to decompressed onchain token accounts, never to compressed accounts. + * For CToken mints, the mint must be decompressed first (CMint account must exist on-chain). + * * The signature matches the standard SPL mintTo for simplicity and consistency. * * @param rpc - RPC connection to use * @param payer - Transaction fee payer - * @param mint - Mint address (SPL, Token-2022, or compressed mint) + * @param mint - Mint address (SPL, Token-2022, or CToken mint) * @param destination - Destination token account address (must be an existing onchain token account) * @param authority - Mint authority (can be Signer or PublicKey if multiSigners provided) * @param amount - Amount to mint * @param multiSigners - Optional: Multi-signature signers (default: []) * @param confirmOptions - Optional: Transaction confirmation options - * @param programId - Optional: Token program ID (TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, or CTOKEN_PROGRAM_ID). If undefined, auto-detects. + * @param programId - Optional: Token program ID. If undefined, auto-detects. * * @returns Transaction signature */ @@ -56,23 +56,6 @@ export async function mintToInterface( programId, ); - // Fetch validity proof if this is a compressed mint (has merkleContext) - let validityProof; - if (mintInterface.merkleContext) { - validityProof = await rpc.getValidityProofV2( - [ - { - hash: bn(mintInterface.merkleContext.hash), - leafIndex: mintInterface.merkleContext.leafIndex, - treeInfo: mintInterface.merkleContext.treeInfo, - proveByIndex: mintInterface.merkleContext.proveByIndex, - }, - ], - [], - DerivationMode.compressible, - ); - } - // Create instruction const authorityPubkey = authority instanceof PublicKey ? authority : authority.publicKey; @@ -84,7 +67,7 @@ export async function mintToInterface( authorityPubkey, payer.publicKey, amount, - validityProof, + undefined, // validityProof - not needed for simple CTokenMintTo multiSignerPubkeys, ); @@ -103,7 +86,7 @@ export async function mintToInterface( const { blockhash } = await rpc.getLatestBlockhash(); const tx = buildAndSignTx( - [ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), ix], + [ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), ix], payer, blockhash, signers, diff --git a/js/compressed-token/src/v3/actions/mint-to.ts b/js/compressed-token/src/v3/actions/mint-to.ts index a9f208d150..f92a6e2268 100644 --- a/js/compressed-token/src/v3/actions/mint-to.ts +++ b/js/compressed-token/src/v3/actions/mint-to.ts @@ -9,95 +9,51 @@ import { Rpc, buildAndSignTx, sendAndConfirmTx, - DerivationMode, - bn, - CTOKEN_PROGRAM_ID, - selectStateTreeInfo, - TreeInfo, assertBetaEnabled, } from '@lightprotocol/stateless.js'; import { createMintToInstruction } from '../instructions/mint-to'; -import { getMintInterface } from '../get-mint-interface'; +/** + * Mint tokens to a CToken account. + * + * This is a simple mint instruction for minting to decompressed CToken accounts. + * The mint must be decompressed (CMint account must exist on-chain). + * + * @param rpc - RPC connection + * @param payer - Fee payer (signer) + * @param mint - Mint address (CMint account) + * @param destination - Destination CToken account + * @param authority - Mint authority (signer) + * @param amount - Amount to mint + * @param maxTopUp - Optional maximum lamports for rent top-up + * @param confirmOptions - Optional confirm options + * @returns Transaction signature + */ export async function mintTo( rpc: Rpc, payer: Signer, mint: PublicKey, - recipientAccount: PublicKey, + destination: PublicKey, authority: Signer, amount: number | bigint, - outputQueue?: PublicKey, + maxTopUp?: number, confirmOptions?: ConfirmOptions, ): Promise { assertBetaEnabled(); - const mintInfo = await getMintInterface( - rpc, - mint, - confirmOptions?.commitment, - CTOKEN_PROGRAM_ID, - ); - - if (!mintInfo.merkleContext) { - throw new Error('Mint does not have MerkleContext'); - } - - let outputStateTreeInfo: TreeInfo; - if (!outputQueue) { - const trees = await rpc.getStateTreeInfos(); - outputStateTreeInfo = selectStateTreeInfo(trees); - } else { - const trees = await rpc.getStateTreeInfos(); - outputStateTreeInfo = trees.find( - t => t.queue.equals(outputQueue!) || t.tree.equals(outputQueue!), - )!; - if (!outputStateTreeInfo) { - throw new Error('Could not find TreeInfo for provided outputQueue'); - } - } + // Use payer as fee payer for top-ups if authority is different from payer + const feePayer = authority.publicKey.equals(payer.publicKey) + ? undefined + : payer.publicKey; - const validityProof = await rpc.getValidityProofV2( - [ - { - hash: bn(mintInfo.merkleContext.hash), - leafIndex: mintInfo.merkleContext.leafIndex, - treeInfo: mintInfo.merkleContext.treeInfo, - proveByIndex: mintInfo.merkleContext.proveByIndex, - }, - ], - [], - DerivationMode.compressible, - ); - - const ix = createMintToInstruction( - authority.publicKey, - payer.publicKey, - validityProof, - mintInfo.merkleContext, - { - supply: mintInfo.mint.supply, - decimals: mintInfo.mint.decimals, - mintAuthority: mintInfo.mint.mintAuthority, - freezeAuthority: mintInfo.mint.freezeAuthority, - splMint: mintInfo.mintContext!.splMint, - cmintDecompressed: mintInfo.mintContext!.cmintDecompressed, - version: mintInfo.mintContext!.version, - mintSigner: mintInfo.mintContext!.mintSigner, - bump: mintInfo.mintContext!.bump, - metadata: mintInfo.tokenMetadata - ? { - updateAuthority: - mintInfo.tokenMetadata.updateAuthority || null, - name: mintInfo.tokenMetadata.name, - symbol: mintInfo.tokenMetadata.symbol, - uri: mintInfo.tokenMetadata.uri, - } - : undefined, - }, - outputStateTreeInfo, - recipientAccount, + const ix = createMintToInstruction({ + mint, + destination, amount, - ); + authority: authority.publicKey, + maxTopUp, + feePayer, + }); const additionalSigners = authority.publicKey.equals(payer.publicKey) ? [] @@ -105,7 +61,7 @@ export async function mintTo( const { blockhash } = await rpc.getLatestBlockhash(); const tx = buildAndSignTx( - [ComputeBudgetProgram.setComputeUnitLimit({ units: 500_000 }), ix], + [ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), ix], payer, blockhash, additionalSigners, diff --git a/js/compressed-token/src/v3/actions/update-metadata.ts b/js/compressed-token/src/v3/actions/update-metadata.ts index c96746a197..8c03686e76 100644 --- a/js/compressed-token/src/v3/actions/update-metadata.ts +++ b/js/compressed-token/src/v3/actions/update-metadata.ts @@ -23,6 +23,7 @@ import { getMintInterface } from '../get-mint-interface'; /** * Update a metadata field on a compressed token mint. + * Works for both compressed and decompressed mints. * * @param rpc RPC connection * @param payer Fee payer (signer) @@ -58,18 +59,23 @@ export async function updateMetadataField( throw new Error('Mint does not have TokenMetadata extension'); } - const validityProof = await rpc.getValidityProofV2( - [ - { - hash: bn(mintInterface.merkleContext.hash), - leafIndex: mintInterface.merkleContext.leafIndex, - treeInfo: mintInterface.merkleContext.treeInfo, - proveByIndex: mintInterface.merkleContext.proveByIndex, - }, - ], - [], - DerivationMode.compressible, - ); + // When mint is decompressed, no validity proof needed - program reads from CMint account + const isDecompressed = + mintInterface.mintContext?.cmintDecompressed ?? false; + const validityProof = isDecompressed + ? null + : await rpc.getValidityProofV2( + [ + { + hash: bn(mintInterface.merkleContext.hash), + leafIndex: mintInterface.merkleContext.leafIndex, + treeInfo: mintInterface.merkleContext.treeInfo, + proveByIndex: mintInterface.merkleContext.proveByIndex, + }, + ], + [], + DerivationMode.compressible, + ); const ix = createUpdateMetadataFieldInstruction( mintInterface, @@ -99,6 +105,7 @@ export async function updateMetadataField( /** * Update the metadata authority of a compressed token mint. + * Works for both compressed and decompressed mints. * * @param rpc RPC connection * @param payer Fee payer (signer) @@ -130,18 +137,23 @@ export async function updateMetadataAuthority( throw new Error('Mint does not have TokenMetadata extension'); } - const validityProof = await rpc.getValidityProofV2( - [ - { - hash: bn(mintInterface.merkleContext.hash), - leafIndex: mintInterface.merkleContext.leafIndex, - treeInfo: mintInterface.merkleContext.treeInfo, - proveByIndex: mintInterface.merkleContext.proveByIndex, - }, - ], - [], - DerivationMode.compressible, - ); + // When mint is decompressed, no validity proof needed - program reads from CMint account + const isDecompressed = + mintInterface.mintContext?.cmintDecompressed ?? false; + const validityProof = isDecompressed + ? null + : await rpc.getValidityProofV2( + [ + { + hash: bn(mintInterface.merkleContext.hash), + leafIndex: mintInterface.merkleContext.leafIndex, + treeInfo: mintInterface.merkleContext.treeInfo, + proveByIndex: mintInterface.merkleContext.proveByIndex, + }, + ], + [], + DerivationMode.compressible, + ); const ix = createUpdateMetadataAuthorityInstruction( mintInterface, @@ -169,6 +181,7 @@ export async function updateMetadataAuthority( /** * Remove a metadata key from a compressed token mint. + * Works for both compressed and decompressed mints. * * @param rpc RPC connection * @param payer Fee payer (signer) @@ -202,18 +215,23 @@ export async function removeMetadataKey( throw new Error('Mint does not have TokenMetadata extension'); } - const validityProof = await rpc.getValidityProofV2( - [ - { - hash: bn(mintInterface.merkleContext.hash), - leafIndex: mintInterface.merkleContext.leafIndex, - treeInfo: mintInterface.merkleContext.treeInfo, - proveByIndex: mintInterface.merkleContext.proveByIndex, - }, - ], - [], - DerivationMode.compressible, - ); + // When mint is decompressed, no validity proof needed - program reads from CMint account + const isDecompressed = + mintInterface.mintContext?.cmintDecompressed ?? false; + const validityProof = isDecompressed + ? null + : await rpc.getValidityProofV2( + [ + { + hash: bn(mintInterface.merkleContext.hash), + leafIndex: mintInterface.merkleContext.leafIndex, + treeInfo: mintInterface.merkleContext.treeInfo, + proveByIndex: mintInterface.merkleContext.proveByIndex, + }, + ], + [], + DerivationMode.compressible, + ); const ix = createRemoveMetadataKeyInstruction( mintInterface, diff --git a/js/compressed-token/src/v3/actions/update-mint.ts b/js/compressed-token/src/v3/actions/update-mint.ts index bd96b28f7f..075702a316 100644 --- a/js/compressed-token/src/v3/actions/update-mint.ts +++ b/js/compressed-token/src/v3/actions/update-mint.ts @@ -22,6 +22,7 @@ import { getMintInterface } from '../get-mint-interface'; /** * Update the mint authority of a compressed token mint. + * Works for both compressed and decompressed mints. * * @param rpc RPC connection * @param payer Fee payer (signer) @@ -51,18 +52,23 @@ export async function updateMintAuthority( throw new Error('Mint does not have MerkleContext'); } - const validityProof = await rpc.getValidityProofV2( - [ - { - hash: bn(mintInterface.merkleContext.hash), - leafIndex: mintInterface.merkleContext.leafIndex, - treeInfo: mintInterface.merkleContext.treeInfo, - proveByIndex: mintInterface.merkleContext.proveByIndex, - }, - ], - [], - DerivationMode.compressible, - ); + // When mint is decompressed, no validity proof needed - program reads from CMint account + const isDecompressed = + mintInterface.mintContext?.cmintDecompressed ?? false; + const validityProof = isDecompressed + ? null + : await rpc.getValidityProofV2( + [ + { + hash: bn(mintInterface.merkleContext.hash), + leafIndex: mintInterface.merkleContext.leafIndex, + treeInfo: mintInterface.merkleContext.treeInfo, + proveByIndex: mintInterface.merkleContext.proveByIndex, + }, + ], + [], + DerivationMode.compressible, + ); const ix = createUpdateMintAuthorityInstruction( mintInterface, @@ -91,6 +97,7 @@ export async function updateMintAuthority( /** * Update the freeze authority of a compressed token mint. + * Works for both compressed and decompressed mints. * * @param rpc RPC connection * @param payer Fee payer (signer) @@ -120,18 +127,23 @@ export async function updateFreezeAuthority( throw new Error('Mint does not have MerkleContext'); } - const validityProof = await rpc.getValidityProofV2( - [ - { - hash: bn(mintInterface.merkleContext.hash), - leafIndex: mintInterface.merkleContext.leafIndex, - treeInfo: mintInterface.merkleContext.treeInfo, - proveByIndex: mintInterface.merkleContext.proveByIndex, - }, - ], - [], - DerivationMode.compressible, - ); + // When mint is decompressed, no validity proof needed - program reads from CMint account + const isDecompressed = + mintInterface.mintContext?.cmintDecompressed ?? false; + const validityProof = isDecompressed + ? null + : await rpc.getValidityProofV2( + [ + { + hash: bn(mintInterface.merkleContext.hash), + leafIndex: mintInterface.merkleContext.leafIndex, + treeInfo: mintInterface.merkleContext.treeInfo, + proveByIndex: mintInterface.merkleContext.proveByIndex, + }, + ], + [], + DerivationMode.compressible, + ); const ix = createUpdateFreezeAuthorityInstruction( mintInterface, diff --git a/js/compressed-token/src/v3/get-mint-interface.ts b/js/compressed-token/src/v3/get-mint-interface.ts index 402ba5ab6f..ee6cffc0e1 100644 --- a/js/compressed-token/src/v3/get-mint-interface.ts +++ b/js/compressed-token/src/v3/get-mint-interface.ts @@ -103,9 +103,31 @@ export async function getMintInterface( ); } - const compressedMintData = deserializeMint( - Buffer.from(compressedAccount.data.data), - ); + const compressedData = Buffer.from(compressedAccount.data.data); + + // After decompressMint, the compressed account contains sentinel data (just hash ~32 bytes). + // The actual mint data lives on-chain in the CMint account. + // Minimum compressed mint size is 82 (base) + 34 (context) + 33 (signer+bump) = 149+ bytes. + const SENTINEL_THRESHOLD = 64; + const isDecompressed = compressedData.length < SENTINEL_THRESHOLD; + + let compressedMintData: CompressedMint; + + if (isDecompressed) { + // Mint is decompressed - read from on-chain CMint account + const cmintAccountInfo = await rpc.getAccountInfo(address); + if (!cmintAccountInfo?.data) { + throw new Error( + `Decompressed CMint account not found on-chain for ${address.toString()}`, + ); + } + compressedMintData = deserializeMint( + Buffer.from(cmintAccountInfo.data), + ); + } else { + // Mint is still compressed - use compressed account data + compressedMintData = deserializeMint(compressedData); + } const mint: Mint = { address, diff --git a/js/compressed-token/src/v3/instructions/decompress-mint.ts b/js/compressed-token/src/v3/instructions/decompress-mint.ts new file mode 100644 index 0000000000..df180c15b3 --- /dev/null +++ b/js/compressed-token/src/v3/instructions/decompress-mint.ts @@ -0,0 +1,239 @@ +import { + PublicKey, + SystemProgram, + TransactionInstruction, +} from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { + ValidityProofWithContext, + CTOKEN_PROGRAM_ID, + LightSystemProgram, + defaultStaticAccountsStruct, + getOutputQueue, +} from '@lightprotocol/stateless.js'; +import { CompressedTokenProgram } from '../../program'; +import { MintInterface } from '../get-mint-interface'; +import { + encodeMintActionInstructionData, + MintActionCompressedInstructionData, + ExtensionInstructionData, +} from '../layout/layout-mint-action'; +import { LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR } from '../../constants'; + +interface EncodeDecompressMintInstructionParams { + leafIndex: number; + proveByIndex: boolean; + rootIndex: number; + proof: { a: number[]; b: number[]; c: number[] } | null; + mintInterface: MintInterface; + rentPayment: number; + writeTopUp: number; +} + +function encodeDecompressMintInstructionData( + params: EncodeDecompressMintInstructionParams, +): Buffer { + // Build extensions if metadata present + let extensions: ExtensionInstructionData[] | null = null; + if (params.mintInterface.tokenMetadata) { + extensions = [ + { + tokenMetadata: { + updateAuthority: + params.mintInterface.tokenMetadata.updateAuthority ?? + null, + name: Buffer.from(params.mintInterface.tokenMetadata.name), + symbol: Buffer.from( + params.mintInterface.tokenMetadata.symbol, + ), + uri: Buffer.from(params.mintInterface.tokenMetadata.uri), + additionalMetadata: null, + }, + }, + ]; + } + + const instructionData: MintActionCompressedInstructionData = { + leafIndex: params.leafIndex, + proveByIndex: params.proveByIndex, + rootIndex: params.rootIndex, + maxTopUp: 0, + createMint: null, + actions: [ + { + decompressMint: { + rentPayment: params.rentPayment, + writeTopUp: params.writeTopUp, + }, + }, + ], + proof: params.proof, + cpiContext: null, + mint: { + supply: params.mintInterface.mint.supply, + decimals: params.mintInterface.mint.decimals, + metadata: { + version: params.mintInterface.mintContext!.version, + cmintDecompressed: + params.mintInterface.mintContext!.cmintDecompressed, + mint: params.mintInterface.mintContext!.splMint, + mintSigner: Array.from( + params.mintInterface.mintContext!.mintSigner, + ), + bump: params.mintInterface.mintContext!.bump, + }, + mintAuthority: params.mintInterface.mint.mintAuthority, + freezeAuthority: params.mintInterface.mint.freezeAuthority, + extensions, + }, + }; + + return encodeMintActionInstructionData(instructionData); +} + +export interface DecompressMintInstructionParams { + /** MintInterface from getMintInterface() - must have merkleContext */ + mintInterface: MintInterface; + /** Authority signer public key (can be any account, decompressMint is permissionless) */ + authority: PublicKey; + /** Fee payer public key */ + payer: PublicKey; + /** Validity proof for the compressed mint */ + validityProof: ValidityProofWithContext; + /** Number of epochs to prepay rent (minimum 2) */ + rentPayment?: number; + /** Per-write top-up in lamports (default: 766) */ + writeTopUp?: number; + /** Compressible config account (default: LIGHT_TOKEN_CONFIG) */ + configAccount?: PublicKey; + /** Rent sponsor PDA (default: LIGHT_TOKEN_RENT_SPONSOR) */ + rentSponsor?: PublicKey; +} + +/** + * Create instruction for decompressing a compressed mint. + * + * This creates the CMint Solana account from a compressed mint, making + * the mint available on-chain. This is required before creating CToken + * associated token accounts. + * + * DecompressMint is **permissionless** - any account can call it. The + * caller pays initial rent, rent exemption is sponsored by the rent_sponsor. + * + * @param params - Instruction parameters + * @returns TransactionInstruction for decompressing the mint + */ +export function createDecompressMintInstruction( + params: DecompressMintInstructionParams, +): TransactionInstruction { + const { + mintInterface, + authority, + payer, + validityProof, + rentPayment = 16, // Default: 16 epochs (~24 hours) + writeTopUp = 766, // Default: ~2 epochs worth + configAccount = LIGHT_TOKEN_CONFIG, + rentSponsor = LIGHT_TOKEN_RENT_SPONSOR, + } = params; + + if (!mintInterface.merkleContext) { + throw new Error( + 'MintInterface must have merkleContext for compressed mint operations', + ); + } + if (!mintInterface.mintContext) { + throw new Error( + 'MintInterface must have mintContext for compressed mint operations', + ); + } + + // Validate rentPayment minimum + if (rentPayment < 2) { + throw new Error('rentPayment must be at least 2 epochs'); + } + + const merkleContext = mintInterface.merkleContext; + const outputQueue = getOutputQueue(merkleContext); + + const data = encodeDecompressMintInstructionData({ + leafIndex: merkleContext.leafIndex, + proveByIndex: true, + rootIndex: validityProof.rootIndices[0], + proof: validityProof.compressedProof, + mintInterface, + rentPayment, + writeTopUp, + }); + + const sys = defaultStaticAccountsStruct(); + + // Account order for decompressMint (needs_compressible_accounts = true): + // 0. light_system_program + // 1. authority (signer) + // 2. compressible_config + // 3. cmint (to be created) + // 4. rent_sponsor + // 5. fee_payer (signer, mut) + // 6. registered_program_pda + // 7. account_compression_authority + // 8. account_compression_program + // 9. system_program + // 10. out_output_queue + // 11. in_merkle_tree + // 12. in_output_queue + const keys = [ + { + pubkey: LightSystemProgram.programId, + isSigner: false, + isWritable: false, + }, + { pubkey: authority, isSigner: true, isWritable: false }, + { pubkey: configAccount, isSigner: false, isWritable: false }, + { + pubkey: mintInterface.mintContext.splMint, + isSigner: false, + isWritable: true, + }, + { pubkey: rentSponsor, isSigner: false, isWritable: true }, + { pubkey: payer, isSigner: true, isWritable: true }, + { + pubkey: CompressedTokenProgram.deriveCpiAuthorityPda, + isSigner: false, + isWritable: false, + }, + { + pubkey: sys.registeredProgramPda, + isSigner: false, + isWritable: false, + }, + { + pubkey: sys.accountCompressionAuthority, + isSigner: false, + isWritable: false, + }, + { + pubkey: sys.accountCompressionProgram, + isSigner: false, + isWritable: false, + }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: outputQueue, isSigner: false, isWritable: true }, + { + pubkey: merkleContext.treeInfo.tree, + isSigner: false, + isWritable: true, + }, + { + pubkey: merkleContext.treeInfo.queue, + isSigner: false, + isWritable: true, + }, + ]; + + return new TransactionInstruction({ + programId: CTOKEN_PROGRAM_ID, + keys, + data, + }); +} diff --git a/js/compressed-token/src/v3/instructions/index.ts b/js/compressed-token/src/v3/instructions/index.ts index c4042bacea..6bdf6984f4 100644 --- a/js/compressed-token/src/v3/instructions/index.ts +++ b/js/compressed-token/src/v3/instructions/index.ts @@ -1,6 +1,7 @@ export * from './create-mint'; export * from './update-mint'; export * from './update-metadata'; +export * from './decompress-mint'; export * from './create-associated-ctoken'; export * from './create-ata-interface'; export * from './mint-to'; diff --git a/js/compressed-token/src/v3/instructions/mint-to-interface.ts b/js/compressed-token/src/v3/instructions/mint-to-interface.ts index 035047230a..5f9f568ea4 100644 --- a/js/compressed-token/src/v3/instructions/mint-to-interface.ts +++ b/js/compressed-token/src/v3/instructions/mint-to-interface.ts @@ -1,8 +1,5 @@ import { PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { - getOutputTreeInfo, - ValidityProofWithContext, -} from '@lightprotocol/stateless.js'; +import { ValidityProofWithContext } from '@lightprotocol/stateless.js'; import { createMintToInstruction as createSplMintToInstruction } from '@solana/spl-token'; import { createMintToInstruction as createCtokenMintToInstruction } from './mint-to'; import { MintInterface } from '../get-mint-interface'; @@ -19,16 +16,18 @@ export interface CreateMintToInterfaceInstructionParams { } /** - * Create mint-to instruction for SPL, Token-2022, or compressed token mints. + * Create mint-to instruction for SPL, Token-2022, or CToken mints. * This instruction ONLY mints to decompressed/onchain token accounts. * - * @param mintInterface Mint interface (SPL, Token-2022, or compressed). + * For CToken mints, the mint must be decompressed first (CMint account must exist on-chain). + * + * @param mintInterface Mint interface (SPL, Token-2022, or CToken). * @param destination Destination onchain token account address. * @param authority Mint authority public key. * @param payer Fee payer public key. * @param amount Amount to mint. - * @param validityProof Validity proof (required for compressed mints). - * @param multiSigners Multi-signature signer public keys. + * @param validityProof Not used (legacy parameter, kept for compatibility). + * @param multiSigners Multi-signature signer public keys (SPL/T22 only). */ export function createMintToInterfaceInstruction( mintInterface: MintInterface, @@ -42,7 +41,7 @@ export function createMintToInterfaceInstruction( const mint = mintInterface.mint.address; const programId = mintInterface.programId; - // SPL/T22 + // SPL/T22 - no merkleContext means it's a native SPL mint if (!mintInterface.merkleContext) { return createSplMintToInstruction( mint, @@ -54,42 +53,20 @@ export function createMintToInterfaceInstruction( ); } - if (!validityProof) { - throw new Error('Validity proof required for c-token mint-to'); - } + // CToken (compressed token) - use simple CTokenMintTo instruction + // The mint must be decompressed for this to work (CMint account must exist on-chain) if (!mintInterface.mintContext) { - throw new Error('mintContext required for c-token mint-to'); + throw new Error('mintContext required for CToken mint-to'); } - const mintData = { - supply: mintInterface.mint.supply, - decimals: mintInterface.mint.decimals, - mintAuthority: mintInterface.mint.mintAuthority, - freezeAuthority: mintInterface.mint.freezeAuthority, - splMint: mintInterface.mintContext.splMint, - cmintDecompressed: mintInterface.mintContext.cmintDecompressed, - version: mintInterface.mintContext.version, - mintSigner: mintInterface.mintContext.mintSigner, - bump: mintInterface.mintContext.bump, - metadata: mintInterface.tokenMetadata - ? { - updateAuthority: - mintInterface.tokenMetadata.updateAuthority || null, - name: mintInterface.tokenMetadata.name, - symbol: mintInterface.tokenMetadata.symbol, - uri: mintInterface.tokenMetadata.uri, - } - : undefined, - }; + // Use payer as fee payer for top-ups if different from authority + const feePayer = authority.equals(payer) ? undefined : payer; - return createCtokenMintToInstruction( - authority, - payer, - validityProof, - mintInterface.merkleContext, - mintData, - getOutputTreeInfo(mintInterface.merkleContext), + return createCtokenMintToInstruction({ + mint, destination, amount, - ); + authority, + feePayer, + }); } diff --git a/js/compressed-token/src/v3/instructions/mint-to.ts b/js/compressed-token/src/v3/instructions/mint-to.ts index bf41b4bda0..99eda509d6 100644 --- a/js/compressed-token/src/v3/instructions/mint-to.ts +++ b/js/compressed-token/src/v3/instructions/mint-to.ts @@ -4,175 +4,68 @@ import { TransactionInstruction, } from '@solana/web3.js'; import { Buffer } from 'buffer'; -import { - ValidityProofWithContext, - CTOKEN_PROGRAM_ID, - LightSystemProgram, - defaultStaticAccountsStruct, - getDefaultAddressTreeInfo, - MerkleContext, - TreeInfo, -} from '@lightprotocol/stateless.js'; -import { CompressedTokenProgram } from '../../program'; -import { MintInstructionData } from '../layout/layout-mint'; -import { - encodeMintActionInstructionData, - MintActionCompressedInstructionData, -} from '../layout/layout-mint-action'; - -interface EncodeMintToCTokenInstructionParams { - addressTree: PublicKey; - leafIndex: number; - rootIndex: number; - proof: { a: number[]; b: number[]; c: number[] } | null; - mintData: MintInstructionData; - recipientAccountIndex: number; - amount: number | bigint; -} +import { CTOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; -function encodeMintToCTokenInstructionData( - params: EncodeMintToCTokenInstructionParams, -): Buffer { - // TokenMetadata extension not supported in mintTo instruction - if (params.mintData.metadata) { - throw new Error( - 'TokenMetadata extension not supported in mintTo instruction', - ); - } - - const instructionData: MintActionCompressedInstructionData = { - leafIndex: params.leafIndex, - proveByIndex: true, - rootIndex: params.rootIndex, - maxTopUp: 0, - createMint: null, - actions: [ - { - mintToCToken: { - accountIndex: params.recipientAccountIndex, - amount: BigInt(params.amount.toString()), - }, - }, - ], - proof: params.proof, - cpiContext: null, - mint: { - supply: params.mintData.supply, - decimals: params.mintData.decimals, - metadata: { - version: params.mintData.version, - cmintDecompressed: params.mintData.cmintDecompressed, - mint: params.mintData.splMint, - mintSigner: Array.from(params.mintData.mintSigner), - bump: params.mintData.bump, - }, - mintAuthority: params.mintData.mintAuthority, - freezeAuthority: params.mintData.freezeAuthority, - extensions: null, - }, - }; - - return encodeMintActionInstructionData(instructionData); -} - -// Keep old interface type for backwards compatibility export +/** + * Parameters for creating a MintTo instruction. + */ export interface CreateMintToInstructionParams { - mintSigner: PublicKey; - authority: PublicKey; - payer: PublicKey; - validityProof: ValidityProofWithContext; - merkleContext: MerkleContext; - mintData: MintInstructionData; - outputStateTreeInfo: TreeInfo; - tokensOutQueue: PublicKey; - recipientAccount: PublicKey; + /** Mint account (CMint - decompressed compressed mint) */ + mint: PublicKey; + /** Destination CToken account to mint to */ + destination: PublicKey; + /** Amount of tokens to mint */ amount: number | bigint; + /** Mint authority (must be signer) */ + authority: PublicKey; + /** Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (0 = no limit) */ + maxTopUp?: number; + /** Optional fee payer for rent top-ups. If not provided, authority pays. */ + feePayer?: PublicKey; } /** - * Create instruction for minting compressed tokens to an onchain token account. + * Create instruction for minting tokens to a CToken account. * - * @param authority Mint authority public key. - * @param payer Fee payer public key. - * @param validityProof Validity proof for the compressed mint. - * @param merkleContext Merkle context of the compressed mint. - * @param mintData Mint instruction data. - * @param outputStateTreeInfo Output state tree info. - * @param recipientAccount Recipient onchain token account address. - * @param amount Amount to mint. + * This is a simple 3-4 account instruction for minting to decompressed CToken accounts. + * Uses discriminator 7 (CTokenMintTo). + * + * @param params - Mint instruction parameters + * @returns TransactionInstruction for minting tokens */ export function createMintToInstruction( - authority: PublicKey, - payer: PublicKey, - validityProof: ValidityProofWithContext, - merkleContext: MerkleContext, - mintData: MintInstructionData, - outputStateTreeInfo: TreeInfo, - recipientAccount: PublicKey, - amount: number | bigint, + params: CreateMintToInstructionParams, ): TransactionInstruction { - const addressTreeInfo = getDefaultAddressTreeInfo(); - const data = encodeMintToCTokenInstructionData({ - addressTree: addressTreeInfo.tree, - leafIndex: merkleContext.leafIndex, - rootIndex: validityProof.rootIndices[0], - proof: validityProof.compressedProof, - mintData, - recipientAccountIndex: 0, - amount, - }); + const { mint, destination, amount, authority, maxTopUp, feePayer } = params; + + // Authority is writable only when maxTopUp is set AND no feePayer + // (authority pays for top-ups only if no separate feePayer) + const authorityWritable = maxTopUp !== undefined && !feePayer; - const sys = defaultStaticAccountsStruct(); const keys = [ - { - pubkey: LightSystemProgram.programId, - isSigner: false, - isWritable: false, - }, - { pubkey: authority, isSigner: true, isWritable: false }, - { pubkey: payer, isSigner: true, isWritable: true }, - { - pubkey: CompressedTokenProgram.deriveCpiAuthorityPda, - isSigner: false, - isWritable: false, - }, - { - pubkey: sys.registeredProgramPda, - isSigner: false, - isWritable: false, - }, - { - pubkey: sys.accountCompressionAuthority, - isSigner: false, - isWritable: false, - }, - { - pubkey: sys.accountCompressionProgram, - isSigner: false, - isWritable: false, - }, + { pubkey: mint, isSigner: false, isWritable: true }, + { pubkey: destination, isSigner: false, isWritable: true }, + { pubkey: authority, isSigner: true, isWritable: authorityWritable }, + // System program required for rent top-up CPIs { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - { - pubkey: outputStateTreeInfo.queue, - isSigner: false, - isWritable: true, - }, - { - pubkey: merkleContext.treeInfo.tree, - isSigner: false, - isWritable: true, - }, - { - pubkey: merkleContext.treeInfo.queue, - isSigner: false, - isWritable: true, - }, - // Note: tokensOutQueue is NOT included for MintToCToken-only actions. - // MintToCToken mints to existing decompressed accounts, doesn't create - // new compressed outputs so Rust expects no tokens_out_queue account. ]; - keys.push({ pubkey: recipientAccount, isSigner: false, isWritable: true }); + // Add fee_payer if provided (must be signer and writable) + if (feePayer) { + keys.push({ pubkey: feePayer, isSigner: true, isWritable: true }); + } + + // Build instruction data: discriminator (7) + amount (u64) + optional max_top_up (u16) + const amountBigInt = BigInt(amount.toString()); + const dataSize = maxTopUp !== undefined ? 11 : 9; // 1 + 8 + optional 2 + const data = Buffer.alloc(dataSize); + + data.writeUInt8(7, 0); // CTokenMintTo discriminator + data.writeBigUInt64LE(amountBigInt, 1); + + if (maxTopUp !== undefined) { + data.writeUInt16LE(maxTopUp, 9); + } return new TransactionInstruction({ programId: CTOKEN_PROGRAM_ID, diff --git a/js/compressed-token/src/v3/instructions/update-metadata.ts b/js/compressed-token/src/v3/instructions/update-metadata.ts index f3fc520957..62cdae007a 100644 --- a/js/compressed-token/src/v3/instructions/update-metadata.ts +++ b/js/compressed-token/src/v3/instructions/update-metadata.ts @@ -89,6 +89,11 @@ function encodeUpdateMetadataInstructionData( ); } + // When mint is decompressed (cmintDecompressed=true), the program reads from CMint account + // so we don't need to include mint data in the instruction + const isDecompressed = + mintInterface.mintContext?.cmintDecompressed ?? false; + const instructionData: MintActionCompressedInstructionData = { leafIndex: params.leafIndex, proveByIndex: params.proof === null, @@ -98,31 +103,41 @@ function encodeUpdateMetadataInstructionData( actions: [convertActionToBorsh(params.action)], proof: params.proof, cpiContext: null, - mint: { - supply: mintInterface.mint.supply, - decimals: mintInterface.mint.decimals, - metadata: { - version: mintInterface.mintContext!.version, - cmintDecompressed: mintInterface.mintContext!.cmintDecompressed, - mint: mintInterface.mintContext!.splMint, - mintSigner: Array.from(mintInterface.mintContext!.mintSigner), - bump: mintInterface.mintContext!.bump, - }, - mintAuthority: mintInterface.mint.mintAuthority, - freezeAuthority: mintInterface.mint.freezeAuthority, - extensions: [ - { - tokenMetadata: { - updateAuthority: - mintInterface.tokenMetadata.updateAuthority ?? null, - name: Buffer.from(mintInterface.tokenMetadata.name), - symbol: Buffer.from(mintInterface.tokenMetadata.symbol), - uri: Buffer.from(mintInterface.tokenMetadata.uri), - additionalMetadata: null, - }, - }, - ], - }, + mint: isDecompressed + ? null + : { + supply: mintInterface.mint.supply, + decimals: mintInterface.mint.decimals, + metadata: { + version: mintInterface.mintContext!.version, + cmintDecompressed: + mintInterface.mintContext!.cmintDecompressed, + mint: mintInterface.mintContext!.splMint, + mintSigner: Array.from( + mintInterface.mintContext!.mintSigner, + ), + bump: mintInterface.mintContext!.bump, + }, + mintAuthority: mintInterface.mint.mintAuthority, + freezeAuthority: mintInterface.mint.freezeAuthority, + extensions: [ + { + tokenMetadata: { + updateAuthority: + mintInterface.tokenMetadata.updateAuthority ?? + null, + name: Buffer.from( + mintInterface.tokenMetadata.name, + ), + symbol: Buffer.from( + mintInterface.tokenMetadata.symbol, + ), + uri: Buffer.from(mintInterface.tokenMetadata.uri), + additionalMetadata: null, + }, + }, + ], + }, }; return encodeMintActionInstructionData(instructionData); @@ -132,7 +147,7 @@ function createUpdateMetadataInstruction( mintInterface: MintInterface, authority: PublicKey, payer: PublicKey, - validityProof: ValidityProofWithContext, + validityProof: ValidityProofWithContext | null, action: UpdateMetadataAction, ): TransactionInstruction { if (!mintInterface.merkleContext) { @@ -153,14 +168,15 @@ function createUpdateMetadataInstruction( const merkleContext = mintInterface.merkleContext; const outputQueue = getOutputQueue(merkleContext); + const isDecompressed = mintInterface.mintContext.cmintDecompressed ?? false; const addressTreeInfo = getDefaultAddressTreeInfo(); const data = encodeUpdateMetadataInstructionData({ splMint: mintInterface.mintContext.splMint, addressTree: addressTreeInfo.tree, leafIndex: merkleContext.leafIndex, - rootIndex: validityProof.rootIndices[0], - proof: validityProof.compressedProof, + rootIndex: validityProof?.rootIndices[0] ?? 0, + proof: isDecompressed ? null : (validityProof?.compressedProof ?? null), mintInterface, action, }); @@ -173,6 +189,16 @@ function createUpdateMetadataInstruction( isWritable: false, }, { pubkey: authority, isSigner: true, isWritable: false }, + // CMint account when decompressed (must come before payer for correct account ordering) + ...(isDecompressed + ? [ + { + pubkey: mintInterface.mint.address, + isSigner: false, + isWritable: true, + }, + ] + : []), { pubkey: payer, isSigner: true, isWritable: true }, { pubkey: CompressedTokenProgram.deriveCpiAuthorityPda, @@ -224,7 +250,7 @@ function createUpdateMetadataInstruction( * @param mintInterface MintInterface from getMintInterface() - must have merkleContext and tokenMetadata * @param authority Metadata update authority public key (must sign) * @param payer Fee payer public key - * @param validityProof Validity proof for the compressed mint + * @param validityProof Validity proof for the compressed mint (null for decompressed mints) * @param fieldType Field to update: 'name', 'symbol', 'uri', or 'custom' * @param value New value for the field * @param customKey Custom key name (required if fieldType is 'custom') @@ -234,7 +260,7 @@ export function createUpdateMetadataFieldInstruction( mintInterface: MintInterface, authority: PublicKey, payer: PublicKey, - validityProof: ValidityProofWithContext, + validityProof: ValidityProofWithContext | null, fieldType: 'name' | 'symbol' | 'uri' | 'custom', value: string, customKey?: string, @@ -274,7 +300,7 @@ export function createUpdateMetadataFieldInstruction( * @param currentAuthority Current metadata update authority public key (must sign) * @param newAuthority New metadata update authority public key * @param payer Fee payer public key - * @param validityProof Validity proof for the compressed mint + * @param validityProof Validity proof for the compressed mint (null for decompressed mints) * @param extensionIndex Extension index (default: 0) */ export function createUpdateMetadataAuthorityInstruction( @@ -282,7 +308,7 @@ export function createUpdateMetadataAuthorityInstruction( currentAuthority: PublicKey, newAuthority: PublicKey, payer: PublicKey, - validityProof: ValidityProofWithContext, + validityProof: ValidityProofWithContext | null, extensionIndex: number = 0, ): TransactionInstruction { const action: UpdateMetadataAction = { @@ -309,7 +335,7 @@ export function createUpdateMetadataAuthorityInstruction( * @param mintInterface MintInterface from getMintInterface() - must have merkleContext and tokenMetadata * @param authority Metadata update authority public key (must sign) * @param payer Fee payer public key - * @param validityProof Validity proof for the compressed mint + * @param validityProof Validity proof for the compressed mint (null for decompressed mints) * @param key Metadata key to remove * @param idempotent If true, don't error if key doesn't exist (default: false) * @param extensionIndex Extension index (default: 0) @@ -318,7 +344,7 @@ export function createRemoveMetadataKeyInstruction( mintInterface: MintInterface, authority: PublicKey, payer: PublicKey, - validityProof: ValidityProofWithContext, + validityProof: ValidityProofWithContext | null, key: string, idempotent: boolean = false, extensionIndex: number = 0, diff --git a/js/compressed-token/src/v3/instructions/update-mint.ts b/js/compressed-token/src/v3/instructions/update-mint.ts index c2a28fdab5..6eb1841472 100644 --- a/js/compressed-token/src/v3/instructions/update-mint.ts +++ b/js/compressed-token/src/v3/instructions/update-mint.ts @@ -42,9 +42,14 @@ function encodeUpdateMintInstructionData( ? { updateMintAuthority: { newAuthority: params.newAuthority } } : { updateFreezeAuthority: { newAuthority: params.newAuthority } }; - // Build extensions if metadata present + // When mint is decompressed (cmintDecompressed=true), the program reads from CMint account + // so we don't need to include mint data in the instruction + const isDecompressed = + params.mintInterface.mintContext?.cmintDecompressed ?? false; + + // Build extensions if metadata present (only needed when not decompressed) let extensions: ExtensionInstructionData[] | null = null; - if (params.mintInterface.tokenMetadata) { + if (!isDecompressed && params.mintInterface.tokenMetadata) { extensions = [ { tokenMetadata: { @@ -71,23 +76,25 @@ function encodeUpdateMintInstructionData( actions: [action], proof: params.proof, cpiContext: null, - mint: { - supply: params.mintInterface.mint.supply, - decimals: params.mintInterface.mint.decimals, - metadata: { - version: params.mintInterface.mintContext!.version, - cmintDecompressed: - params.mintInterface.mintContext!.cmintDecompressed, - mint: params.mintInterface.mintContext!.splMint, - mintSigner: Array.from( - params.mintInterface.mintContext!.mintSigner, - ), - bump: params.mintInterface.mintContext!.bump, - }, - mintAuthority: params.mintInterface.mint.mintAuthority, - freezeAuthority: params.mintInterface.mint.freezeAuthority, - extensions, - }, + mint: isDecompressed + ? null + : { + supply: params.mintInterface.mint.supply, + decimals: params.mintInterface.mint.decimals, + metadata: { + version: params.mintInterface.mintContext!.version, + cmintDecompressed: + params.mintInterface.mintContext!.cmintDecompressed, + mint: params.mintInterface.mintContext!.splMint, + mintSigner: Array.from( + params.mintInterface.mintContext!.mintSigner, + ), + bump: params.mintInterface.mintContext!.bump, + }, + mintAuthority: params.mintInterface.mint.mintAuthority, + freezeAuthority: params.mintInterface.mint.freezeAuthority, + extensions, + }, }; return encodeMintActionInstructionData(instructionData); @@ -95,19 +102,20 @@ function encodeUpdateMintInstructionData( /** * Create instruction for updating a compressed mint's mint authority. + * Works for both compressed and decompressed mints. * * @param mintInterface MintInterface from getMintInterface() - must have merkleContext * @param currentMintAuthority Current mint authority public key (must sign) * @param newMintAuthority New mint authority (or null to revoke) * @param payer Fee payer public key - * @param validityProof Validity proof for the compressed mint + * @param validityProof Validity proof for the compressed mint (null for decompressed mints) */ export function createUpdateMintAuthorityInstruction( mintInterface: MintInterface, currentMintAuthority: PublicKey, newMintAuthority: PublicKey | null, payer: PublicKey, - validityProof: ValidityProofWithContext, + validityProof: ValidityProofWithContext | null, ): TransactionInstruction { if (!mintInterface.merkleContext) { throw new Error( @@ -122,6 +130,7 @@ export function createUpdateMintAuthorityInstruction( const merkleContext = mintInterface.merkleContext; const outputQueue = getOutputQueue(merkleContext); + const isDecompressed = mintInterface.mintContext.cmintDecompressed ?? false; const addressTreeInfo = getDefaultAddressTreeInfo(); const data = encodeUpdateMintInstructionData({ @@ -129,8 +138,8 @@ export function createUpdateMintAuthorityInstruction( addressTree: addressTreeInfo.tree, leafIndex: merkleContext.leafIndex, proveByIndex: true, - rootIndex: validityProof.rootIndices[0], - proof: validityProof.compressedProof, + rootIndex: validityProof?.rootIndices[0] ?? 0, + proof: isDecompressed ? null : (validityProof?.compressedProof ?? null), mintInterface, newAuthority: newMintAuthority, actionType: 'mintAuthority', @@ -144,6 +153,16 @@ export function createUpdateMintAuthorityInstruction( isWritable: false, }, { pubkey: currentMintAuthority, isSigner: true, isWritable: false }, + // CMint account when decompressed (must come before payer for correct account ordering) + ...(isDecompressed + ? [ + { + pubkey: mintInterface.mint.address, + isSigner: false, + isWritable: true, + }, + ] + : []), { pubkey: payer, isSigner: true, isWritable: true }, { pubkey: CompressedTokenProgram.deriveCpiAuthorityPda, @@ -188,6 +207,7 @@ export function createUpdateMintAuthorityInstruction( /** * Create instruction for updating a compressed mint's freeze authority. + * Works for both compressed and decompressed mints. * * Output queue is automatically derived from mintInterface.merkleContext.treeInfo * (preferring nextTreeInfo.queue if available for rollover support). @@ -196,14 +216,14 @@ export function createUpdateMintAuthorityInstruction( * @param currentFreezeAuthority Current freeze authority public key (must sign) * @param newFreezeAuthority New freeze authority (or null to revoke) * @param payer Fee payer public key - * @param validityProof Validity proof for the compressed mint + * @param validityProof Validity proof for the compressed mint (null for decompressed mints) */ export function createUpdateFreezeAuthorityInstruction( mintInterface: MintInterface, currentFreezeAuthority: PublicKey, newFreezeAuthority: PublicKey | null, payer: PublicKey, - validityProof: ValidityProofWithContext, + validityProof: ValidityProofWithContext | null, ): TransactionInstruction { if (!mintInterface.merkleContext) { throw new Error( @@ -218,6 +238,7 @@ export function createUpdateFreezeAuthorityInstruction( const merkleContext = mintInterface.merkleContext; const outputQueue = getOutputQueue(merkleContext); + const isDecompressed = mintInterface.mintContext.cmintDecompressed ?? false; const addressTreeInfo = getDefaultAddressTreeInfo(); const data = encodeUpdateMintInstructionData({ @@ -225,8 +246,8 @@ export function createUpdateFreezeAuthorityInstruction( addressTree: addressTreeInfo.tree, leafIndex: merkleContext.leafIndex, proveByIndex: true, - rootIndex: validityProof.rootIndices[0], - proof: validityProof.compressedProof, + rootIndex: validityProof?.rootIndices[0] ?? 0, + proof: isDecompressed ? null : (validityProof?.compressedProof ?? null), mintInterface, newAuthority: newFreezeAuthority, actionType: 'freezeAuthority', @@ -240,6 +261,16 @@ export function createUpdateFreezeAuthorityInstruction( isWritable: false, }, { pubkey: currentFreezeAuthority, isSigner: true, isWritable: false }, + // CMint account when decompressed (must come before payer for correct account ordering) + ...(isDecompressed + ? [ + { + pubkey: mintInterface.mint.address, + isSigner: false, + isWritable: true, + }, + ] + : []), { pubkey: payer, isSigner: true, isWritable: true }, { pubkey: CompressedTokenProgram.deriveCpiAuthorityPda, diff --git a/js/compressed-token/tests/e2e/create-associated-ctoken.test.ts b/js/compressed-token/tests/e2e/create-associated-ctoken.test.ts index 312d43d4f4..1948b9c3d3 100644 --- a/js/compressed-token/tests/e2e/create-associated-ctoken.test.ts +++ b/js/compressed-token/tests/e2e/create-associated-ctoken.test.ts @@ -8,7 +8,7 @@ import { featureFlags, getDefaultAddressTreeInfo, } from '@lightprotocol/stateless.js'; -import { createMintInterface } from '../../src/v3/actions'; +import { createMintInterface, decompressMint } from '../../src/v3/actions'; import { createAssociatedCTokenAccount, createAssociatedCTokenAccountIdempotent, @@ -47,6 +47,10 @@ describe('createAssociatedCTokenAccount', () => { ); await rpc.confirmTransaction(createMintSig, 'confirmed'); + // Decompress mint so it exists on-chain (required for ATA creation) + const decompressSig = await decompressMint(rpc, payer, mintPda); + await rpc.confirmTransaction(decompressSig, 'confirmed'); + const ataAddress = await createAssociatedCTokenAccount( rpc, payer, @@ -86,6 +90,10 @@ describe('createAssociatedCTokenAccount', () => { ); await rpc.confirmTransaction(createMintSig, 'confirmed'); + // Decompress mint so it exists on-chain (required for ATA creation) + const decompressSig = await decompressMint(rpc, payer, mintPda); + await rpc.confirmTransaction(decompressSig, 'confirmed'); + await createAssociatedCTokenAccount( rpc, payer, @@ -117,6 +125,10 @@ describe('createAssociatedCTokenAccount', () => { ); await rpc.confirmTransaction(createMintSig, 'confirmed'); + // Decompress mint so it exists on-chain (required for ATA creation) + const decompressSig = await decompressMint(rpc, payer, mintPda); + await rpc.confirmTransaction(decompressSig, 'confirmed'); + const ataAddress1 = await createAssociatedCTokenAccountIdempotent( rpc, payer, @@ -164,6 +176,10 @@ describe('createAssociatedCTokenAccount', () => { ); await rpc.confirmTransaction(createMintSig, 'confirmed'); + // Decompress mint so it exists on-chain (required for ATA creation) + const decompressSig = await decompressMint(rpc, payer, mintPda); + await rpc.confirmTransaction(decompressSig, 'confirmed'); + const ata1 = await createAssociatedCTokenAccount( rpc, payer, @@ -226,6 +242,10 @@ describe('createAssociatedCTokenAccount', () => { ); await rpc.confirmTransaction(createMintSig, 'confirmed'); + // Decompress mint so it exists on-chain (required for ATA creation) + const decompressSig = await decompressMint(rpc, payer, mintPda); + await rpc.confirmTransaction(decompressSig, 'confirmed'); + const createPromises = Array(3) .fill(null) .map(() => @@ -298,6 +318,10 @@ describe('createMint -> createAssociatedCTokenAccount flow', () => { expect(mint.toString()).toBe(mintPda.toString()); + // Decompress mint so it exists on-chain (required for ATA creation) + const decompressSig = await decompressMint(rpc, payer, mintPda); + await rpc.confirmTransaction(decompressSig, 'confirmed'); + const owner1 = Keypair.generate(); const owner2 = Keypair.generate(); @@ -354,6 +378,10 @@ describe('createMint -> createAssociatedCTokenAccount flow', () => { ); await rpc.confirmTransaction(createMintSig, 'confirmed'); + // Decompress mint so it exists on-chain (required for ATA creation) + const decompressSig = await decompressMint(rpc, payer, mintPda); + await rpc.confirmTransaction(decompressSig, 'confirmed'); + const ataAddress = await createAssociatedCTokenAccountIdempotent( rpc, payer, @@ -388,6 +416,10 @@ describe('createMint -> createAssociatedCTokenAccount flow', () => { ); await rpc.confirmTransaction(createMint1Sig, 'confirmed'); + // Decompress mint1 so it exists on-chain (required for ATA creation) + const decompressSig1 = await decompressMint(rpc, payer, mintPda1); + await rpc.confirmTransaction(decompressSig1, 'confirmed'); + const mintSigner2 = Keypair.generate(); const mintAuthority2 = Keypair.generate(); const [mintPda2] = findMintAddress(mintSigner2.publicKey); @@ -403,6 +435,10 @@ describe('createMint -> createAssociatedCTokenAccount flow', () => { ); await rpc.confirmTransaction(createMint2Sig, 'confirmed'); + // Decompress mint2 so it exists on-chain (required for ATA creation) + const decompressSig2 = await decompressMint(rpc, payer, mintPda2); + await rpc.confirmTransaction(decompressSig2, 'confirmed'); + const ata1 = await createAssociatedCTokenAccount( rpc, payer, @@ -450,6 +486,10 @@ describe('createMint -> createAssociatedCTokenAccount flow', () => { ); await rpc.confirmTransaction(createMintSig, 'confirmed'); + // Decompress mint so it exists on-chain (required for ATA creation) + const decompressSig = await decompressMint(rpc, payer, mintPda); + await rpc.confirmTransaction(decompressSig, 'confirmed'); + await new Promise(resolve => setTimeout(resolve, 1000)); const owner = Keypair.generate(); @@ -486,6 +526,10 @@ describe('createMint -> createAssociatedCTokenAccount flow', () => { ); await rpc.confirmTransaction(createMintSig, 'confirmed'); + // Decompress mint so it exists on-chain (required for ATA creation) + const decompressSig = await decompressMint(rpc, payer, mintPda); + await rpc.confirmTransaction(decompressSig, 'confirmed'); + const ataAddress1 = await createAssociatedCTokenAccountIdempotent( rpc, payer, diff --git a/js/compressed-token/tests/e2e/create-ata-interface.test.ts b/js/compressed-token/tests/e2e/create-ata-interface.test.ts index ea9ceb4034..199ca24892 100644 --- a/js/compressed-token/tests/e2e/create-ata-interface.test.ts +++ b/js/compressed-token/tests/e2e/create-ata-interface.test.ts @@ -16,7 +16,7 @@ import { getAssociatedTokenAddressSync, ASSOCIATED_TOKEN_PROGRAM_ID, } from '@solana/spl-token'; -import { createMintInterface } from '../../src/v3/actions'; +import { createMintInterface, decompressMint } from '../../src/v3/actions'; import { createAtaInterface, createAtaInterfaceIdempotent, @@ -58,6 +58,9 @@ describe('createAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, mintPda); + const address = await createAtaInterface( rpc, payer, @@ -93,6 +96,9 @@ describe('createAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, mintPda); + const address = await createAtaInterface( rpc, payer, @@ -127,6 +133,9 @@ describe('createAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, mintPda); + // Get rent sponsor balance before ATA creation const rentSponsorBalanceBefore = await rpc.getBalance( LIGHT_TOKEN_RENT_SPONSOR, @@ -181,6 +190,9 @@ describe('createAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, mintPda); + // Get balances before const rentSponsorBalanceBefore = await rpc.getBalance( LIGHT_TOKEN_RENT_SPONSOR, @@ -256,6 +268,9 @@ describe('createAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, mintPda); + await createAtaInterface(rpc, payer, mintPda, owner.publicKey); await expect( @@ -278,6 +293,9 @@ describe('createAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, mintPda); + const addr1 = await createAtaInterfaceIdempotent( rpc, payer, @@ -319,6 +337,9 @@ describe('createAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, mintPda); + const addr1 = await createAtaInterface( rpc, payer, @@ -570,6 +591,9 @@ describe('createAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, mintPda); + const address = await createAtaInterface( rpc, payer, @@ -656,6 +680,9 @@ describe('createAtaInterface', () => { mintSigner, ); + // Decompress CToken mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, ctokenMint); + // Create ATAs for both const splAta = await createAtaInterfaceIdempotent( rpc, @@ -753,6 +780,8 @@ describe('createAtaInterface', () => { 9, mintSigner, ); + // Decompress CToken mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, ctokenMint); const ctokenAta = await createAtaInterfaceIdempotent( rpc, payer, @@ -783,6 +812,9 @@ describe('createAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, mintPda); + const promises = Array(3) .fill(null) .map(() => diff --git a/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts b/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts index 57be60ed29..a3c0895a67 100644 --- a/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts +++ b/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts @@ -22,6 +22,7 @@ import { import { createMintInterface } from '../../src/v3/actions/create-mint-interface'; import { createAtaInterfaceIdempotent } from '../../src/v3/actions/create-ata-interface'; import { getOrCreateAtaInterface } from '../../src/v3/actions/get-or-create-ata-interface'; +import { decompressMint } from '../../src/v3/actions/decompress-mint'; import { getAssociatedTokenAddressInterface } from '../../src/v3/get-associated-token-address-interface'; import { findMintAddress } from '../../src/v3/derivation'; import { getAtaProgramId } from '../../src/v3/ata-utils'; @@ -432,6 +433,9 @@ describe('getOrCreateAtaInterface', () => { mintSigner, ); ctokenMint = mintPda; + + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, ctokenMint); }); it('should create c-token ATA when it does not exist (uninited)', async () => { @@ -565,6 +569,9 @@ describe('getOrCreateAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, testMint); + // Create ATA await createAtaInterfaceIdempotent( rpc, @@ -619,11 +626,15 @@ describe('getOrCreateAtaInterface', () => { ); // Mint compressed tokens directly (creates cold balance, no hot ATA) + // Must happen BEFORE decompressMint since mintToCompressed needs compressed mint const mintAmount = 1000000n; await mintToCompressed(rpc, payer, testMint, testMintAuth, [ { recipient: owner.publicKey, amount: mintAmount }, ]); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, testMint); + const expectedAddress = getAssociatedTokenAddressInterface( testMint, owner.publicKey, @@ -688,11 +699,15 @@ describe('getOrCreateAtaInterface', () => { ); // Mint compressed tokens directly (creates cold balance, no hot ATA) + // Must happen BEFORE decompressMint since mintToCompressed needs compressed mint const mintAmount = 1000000n; await mintToCompressed(rpc, payer, testMint, testMintAuth, [ { recipient: owner.publicKey, amount: mintAmount }, ]); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, testMint); + const expectedAddress = getAssociatedTokenAddressInterface( testMint, owner.publicKey, @@ -772,7 +787,17 @@ describe('getOrCreateAtaInterface', () => { mintSigner, ); - // Create hot ATA first + // Mint compressed tokens first (creates cold balance) + // Must happen BEFORE decompressMint since mintToCompressed needs compressed mint + const coldAmount = 500000n; + await mintToCompressed(rpc, payer, testMint, testMintAuth, [ + { recipient: owner.publicKey, amount: coldAmount }, + ]); + + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, testMint); + + // Create hot ATA (after decompression) await createAtaInterfaceIdempotent( rpc, payer, @@ -783,12 +808,6 @@ describe('getOrCreateAtaInterface', () => { CTOKEN_PROGRAM_ID, ); - // Mint compressed tokens (creates cold balance) - const coldAmount = 500000n; - await mintToCompressed(rpc, payer, testMint, testMintAuth, [ - { recipient: owner.publicKey, amount: coldAmount }, - ]); - // Call getOrCreateAtaInterface const account = await getOrCreateAtaInterface( rpc, @@ -819,6 +838,9 @@ describe('getOrCreateAtaInterface', () => { 9, ); ctokenMint = result.mint; + + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, ctokenMint); }); it('should default to CTOKEN_PROGRAM_ID when programId not specified', async () => { @@ -923,6 +945,9 @@ describe('getOrCreateAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, testMint); + const account1 = await getOrCreateAtaInterface( rpc, payer, @@ -992,6 +1017,9 @@ describe('getOrCreateAtaInterface', () => { mintSigner, ); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, ctokenMint); + // Get/Create ATAs for all programs const splAccount = await getOrCreateAtaInterface( rpc, diff --git a/js/compressed-token/tests/e2e/mint-to-ctoken.test.ts b/js/compressed-token/tests/e2e/mint-to-ctoken.test.ts index 18dca890f5..94ba4d4310 100644 --- a/js/compressed-token/tests/e2e/mint-to-ctoken.test.ts +++ b/js/compressed-token/tests/e2e/mint-to-ctoken.test.ts @@ -13,7 +13,7 @@ import { featureFlags, CTOKEN_PROGRAM_ID, } from '@lightprotocol/stateless.js'; -import { createMintInterface } from '../../src/v3/actions/create-mint-interface'; +import { createMintInterface, decompressMint } from '../../src/v3/actions'; import { mintTo } from '../../src/v3/actions/mint-to'; import { getMintInterface } from '../../src/v3/get-mint-interface'; import { createAssociatedCTokenAccount } from '../../src/v3/actions/create-associated-ctoken'; @@ -52,6 +52,9 @@ describe('mintTo (MintToCToken)', () => { await rpc.confirmTransaction(result.transactionSignature, 'confirmed'); mint = result.mint; + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, mint); + await createAssociatedCTokenAccount( rpc, payer, diff --git a/js/compressed-token/tests/e2e/mint-to-interface.test.ts b/js/compressed-token/tests/e2e/mint-to-interface.test.ts index 55810b05ba..9b786364a3 100644 --- a/js/compressed-token/tests/e2e/mint-to-interface.test.ts +++ b/js/compressed-token/tests/e2e/mint-to-interface.test.ts @@ -14,7 +14,7 @@ import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; -import { createMintInterface } from '../../src/v3/actions/create-mint-interface'; +import { createMintInterface, decompressMint } from '../../src/v3/actions'; import { mintToInterface } from '../../src/v3/actions/mint-to-interface'; import { createMint } from '../../src/actions/create-mint'; import { createAssociatedCTokenAccount } from '../../src/v3/actions/create-associated-ctoken'; @@ -193,6 +193,9 @@ describe('mintToInterface - Compressed Mints', () => { ); await rpc.confirmTransaction(result.transactionSignature, 'confirmed'); mint = result.mint; + + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, mint); }); it('should mint compressed tokens to onchain ctoken account', async () => { @@ -486,6 +489,9 @@ describe('mintToInterface - Edge Cases', () => { ); await rpc.confirmTransaction(result.transactionSignature, 'confirmed'); compressedMint = result.mint; + + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, compressedMint); }); it('should handle zero amount minting', async () => { @@ -533,6 +539,9 @@ describe('mintToInterface - Edge Cases', () => { ); await rpc.confirmTransaction(result.transactionSignature, 'confirmed'); + // Decompress mint so it exists on-chain (required for ATA creation) + await decompressMint(rpc, payer, result.mint); + const recipient = Keypair.generate(); await createAssociatedCTokenAccount( rpc, diff --git a/js/compressed-token/tests/e2e/mint-workflow.test.ts b/js/compressed-token/tests/e2e/mint-workflow.test.ts index 45e8adc48d..89097ed951 100644 --- a/js/compressed-token/tests/e2e/mint-workflow.test.ts +++ b/js/compressed-token/tests/e2e/mint-workflow.test.ts @@ -9,7 +9,7 @@ import { getDefaultAddressTreeInfo, CTOKEN_PROGRAM_ID, } from '@lightprotocol/stateless.js'; -import { createMintInterface } from '../../src/v3/actions'; +import { createMintInterface, decompressMint } from '../../src/v3/actions'; import { createTokenMetadata } from '../../src/v3/instructions'; import { updateMintAuthority, @@ -183,6 +183,9 @@ describe('Complete Mint Workflow', () => { const owner2 = Keypair.generate(); const owner3 = Keypair.generate(); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, mint); + const ata1 = await createAtaInterfaceIdempotent( rpc, payer, @@ -298,6 +301,9 @@ describe('Complete Mint Workflow', () => { mintAuthority.publicKey.toString(), ); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, mintPda); + const owner = Keypair.generate(); const ataAddress = await createAtaInterfaceIdempotent( rpc, @@ -342,6 +348,9 @@ describe('Complete Mint Workflow', () => { ); expect(mintInfo.tokenMetadata).toBeUndefined(); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, mint); + const owners = [ Keypair.generate(), Keypair.generate(), @@ -394,6 +403,9 @@ describe('Complete Mint Workflow', () => { ); await rpc.confirmTransaction(createSig, 'confirmed'); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, mintPda); + const owner = Keypair.generate(); const ataAddress = await createAtaInterfaceIdempotent( rpc, @@ -484,6 +496,9 @@ describe('Complete Mint Workflow', () => { expect(mintInfo.mintContext).toBeDefined(); expect(mintInfo.mintContext?.version).toBeGreaterThan(0); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, mint); + const owner1 = Keypair.generate(); const owner2 = Keypair.generate(); @@ -583,6 +598,9 @@ describe('Complete Mint Workflow', () => { expect(mintInfo.mint.freezeAuthority).toBe(null); expect(mintInfo.tokenMetadata).toBeUndefined(); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, mint); + const owner = Keypair.generate(); const ataAddress = await createAtaInterfaceIdempotent( rpc, @@ -647,6 +665,9 @@ describe('Complete Mint Workflow', () => { owner.publicKey, ); + // Decompress mint so it exists on-chain (required for CToken ATA creation) + await decompressMint(rpc, payer, mint); + const ataAddress = await createAtaInterfaceIdempotent( rpc, payer, diff --git a/program-tests/compressed-token-test/tests/light_token/burn.rs b/program-tests/compressed-token-test/tests/light_token/burn.rs index ccc43cbbdd..4ad447582d 100644 --- a/program-tests/compressed-token-test/tests/light_token/burn.rs +++ b/program-tests/compressed-token-test/tests/light_token/burn.rs @@ -319,8 +319,8 @@ struct BurnTestContext { /// /// Steps: /// 1. Init LightProgramTest -/// 2. Create compressed mint + Mint via mint_action_comprehensive -/// 3. Create Light Token ATA +/// 2. Create compressed mint + Mint via mint_action_comprehensive (mint must exist first) +/// 3. Create Light Token ATA (now that mint exists) /// 4. Mint 100 tokens async fn setup_burn_test() -> BurnTestContext { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2(false, None)) @@ -335,19 +335,7 @@ async fn setup_burn_test() -> BurnTestContext { // Derive Mint PDA let (mint_pda, _) = find_mint_address(&mint_seed.pubkey()); - // Step 1: Create Light Token ATA for owner - let ctoken_ata = derive_token_ata(&owner_keypair.pubkey(), &mint_pda); - - let create_ata_ix = - CreateAssociatedTokenAccount::new(payer.pubkey(), owner_keypair.pubkey(), mint_pda) - .instruction() - .unwrap(); - - rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) - .await - .unwrap(); - - // Step 2: Create compressed mint + Mint (no recipients) + // Step 1: Create compressed mint + Mint (must happen before ATA creation) light_test_utils::actions::mint_action_comprehensive( &mut rpc, &mint_seed, @@ -373,6 +361,18 @@ async fn setup_burn_test() -> BurnTestContext { .await .unwrap(); + // Step 2: Create Light Token ATA for owner (mint now exists) + let ctoken_ata = derive_token_ata(&owner_keypair.pubkey(), &mint_pda); + + let create_ata_ix = + CreateAssociatedTokenAccount::new(payer.pubkey(), owner_keypair.pubkey(), mint_pda) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + // Step 3: Mint 100 tokens to the Light Token account let mint_ix = MintTo { mint: mint_pda, diff --git a/program-tests/compressed-token-test/tests/light_token/create_ata.rs b/program-tests/compressed-token-test/tests/light_token/create_ata.rs index 00862f3d2f..6051155b14 100644 --- a/program-tests/compressed-token-test/tests/light_token/create_ata.rs +++ b/program-tests/compressed-token-test/tests/light_token/create_ata.rs @@ -31,7 +31,7 @@ async fn test_create_compressible_ata() { // Test 2: Two epoch prefunding { // Use different mint for second ATA - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; let compressible_data = CompressibleData { compression_authority: context.compression_authority, @@ -55,7 +55,7 @@ async fn test_create_compressible_ata() { // Test 3: Ten epoch prefunding { // Use different mint for third ATA - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; let compressible_data = CompressibleData { compression_authority: context.compression_authority, @@ -79,7 +79,7 @@ async fn test_create_compressible_ata() { // Test 4: Custom fee payer (payer == rent_sponsor, payer pays everything) { // Use different mint for fourth ATA - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; let compressible_data = CompressibleData { compression_authority: context.compression_authority, @@ -103,7 +103,7 @@ async fn test_create_compressible_ata() { // Test 5: No lamports_per_write { // Use different mint for fifth ATA - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; let compressible_data = CompressibleData { compression_authority: context.compression_authority, @@ -127,7 +127,7 @@ async fn test_create_compressible_ata() { // Test 6: Maximum prepaid epochs (255) - boundary test { // Use different mint for sixth ATA - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; let compressible_data = CompressibleData { compression_authority: context.compression_authority, @@ -152,8 +152,8 @@ async fn test_create_compressible_ata() { // This is an unusual but valid configuration where the owner of the ATA // is the same pubkey as the mint. Should succeed. { - // Use a new unique pubkey that will serve as both owner and mint - let owner_and_mint = solana_sdk::pubkey::Pubkey::new_unique(); + // Create a real mint and use its pubkey as both owner and mint + let owner_and_mint = create_additional_mint(&mut context.rpc, &context.payer).await; context.mint_pubkey = owner_and_mint; // Temporarily change the owner keypair to use the same pubkey as mint @@ -328,7 +328,7 @@ async fn test_create_ata_failing() { // Error: 18 (IllegalOwner - account is no longer owned by system program) { // Use a different mint for this test - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; let compressible_data = CompressibleData { compression_authority: context.compression_authority, @@ -375,7 +375,7 @@ async fn test_create_ata_failing() { let poor_payer_pubkey = poor_payer.pubkey(); // Use different mint and owner for this test - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; let new_owner = solana_sdk::signature::Keypair::new(); let compressible_params = CompressibleParams { @@ -418,7 +418,7 @@ async fn test_create_ata_failing() { use solana_sdk::instruction::Instruction; // Use different mint for this test - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; let ata_pubkey = derive_token_ata(&context.owner_keypair.pubkey(), &context.mint_pubkey); // Manually build instruction data with compress_to_account_pubkey (forbidden for ATAs) @@ -487,7 +487,7 @@ async fn test_create_ata_failing() { use solana_sdk::instruction::Instruction; // Use different mint for this test - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; // Use a wrong ATA address (random pubkey instead of derived) let wrong_ata_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); @@ -542,7 +542,7 @@ async fn test_create_ata_failing() { // Error: 14 (InvalidAccountOwner) { // Use different mint for this test - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; // Use system program pubkey as fake config (wrong owner) let fake_config = solana_sdk::system_program::ID; @@ -581,7 +581,7 @@ async fn test_create_ata_failing() { // Error: 18042 (WriteTopUpExceedsMaximum from TokenError) { // Use different mint for this test - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; // Default max_top_up is 6208, so use 6209 to exceed it let excessive_lamports_per_write = RentConfig::default().max_top_up as u32 + 1; @@ -612,7 +612,7 @@ async fn test_create_ata_failing() { // Error: 20000 (InvalidDiscriminator from account-checks) { // Use different mint for this test - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; // Use protocol config account - owned by registry but wrong type let wrong_account_type = context.rpc.test_accounts.protocol.governance_authority_pda; @@ -651,7 +651,7 @@ async fn test_create_ata_failing() { // making the token account impossible to close (lamport transfers to executable accounts fail). // Error: 8 (MissingRequiredSignature) { - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; // Use system program as custom rent payer (executable, cannot sign) let executable_rent_payer = ACCOUNT_COMPRESSION_PROGRAM_ID.into(); @@ -693,7 +693,7 @@ async fn test_create_ata_failing() { use solana_sdk::instruction::Instruction; // Use different mint for this test - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; // Create an arbitrary keypair (NOT the correct PDA) let fake_ata_keypair = solana_sdk::signature::Keypair::new(); @@ -873,9 +873,9 @@ async fn test_ata_multiple_mints_same_owner() { let owner = context.owner_keypair.pubkey(); // Create 3 different ATAs for the same owner with different mints - let mint1 = solana_sdk::pubkey::Pubkey::new_unique(); - let mint2 = solana_sdk::pubkey::Pubkey::new_unique(); - let mint3 = solana_sdk::pubkey::Pubkey::new_unique(); + let mint1 = create_additional_mint(&mut context.rpc, &context.payer).await; + let mint2 = create_additional_mint(&mut context.rpc, &context.payer).await; + let mint3 = create_additional_mint(&mut context.rpc, &context.payer).await; let compressible_data = CompressibleData { compression_authority: context.compression_authority, @@ -938,7 +938,7 @@ async fn test_ata_multiple_owners_same_mint() { let payer_pubkey = context.payer.pubkey(); // Use the same mint for all ATAs - let mint = solana_sdk::pubkey::Pubkey::new_unique(); + let mint = create_additional_mint(&mut context.rpc, &context.payer).await; context.mint_pubkey = mint; // Create 3 different owners @@ -1082,7 +1082,7 @@ async fn test_create_ata_random() { } // Use different mint for each iteration - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; let compressible_data = CompressibleData { compression_authority: context.compression_authority, // Config account forces this authority. diff --git a/program-tests/compressed-token-test/tests/light_token/create_ata2.rs b/program-tests/compressed-token-test/tests/light_token/create_ata2.rs index f547ccc448..d19b771efb 100644 --- a/program-tests/compressed-token-test/tests/light_token/create_ata2.rs +++ b/program-tests/compressed-token-test/tests/light_token/create_ata2.rs @@ -95,7 +95,7 @@ async fn test_create_ata2_basic() { } { - context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = create_additional_mint(&mut context.rpc, &context.payer).await; // All accounts now have compression infrastructure, so pass CompressibleData // with 0 prepaid epochs (immediately compressible) let compressible_data = CompressibleData { diff --git a/program-tests/compressed-token-test/tests/light_token/shared.rs b/program-tests/compressed-token-test/tests/light_token/shared.rs index d2085a66a3..f1d65ad313 100644 --- a/program-tests/compressed-token-test/tests/light_token/shared.rs +++ b/program-tests/compressed-token-test/tests/light_token/shared.rs @@ -13,6 +13,7 @@ pub use light_test_utils::{ }, assert_ctoken_approve_revoke::{assert_ctoken_approve, assert_ctoken_revoke}, assert_transfer2::assert_transfer2_compress, + spl::create_mint_helper, Rpc, RpcError, }; pub use light_token::instruction::{ @@ -35,9 +36,9 @@ pub struct AccountTestContext { /// Set up test environment with common accounts and context pub async fn setup_account_test() -> Result { - let rpc = LightProgramTest::new(ProgramTestConfig::new_v2(false, None)).await?; + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2(false, None)).await?; let payer = rpc.get_payer().insecure_clone(); - let mint_pubkey = Pubkey::new_unique(); + let mint_pubkey = create_mint_helper(&mut rpc, &payer).await; let owner_keypair = Keypair::new(); let token_account_keypair = Keypair::new(); @@ -59,6 +60,11 @@ pub async fn setup_account_test() -> Result { }) } +/// Create an additional SPL mint for tests that need multiple mints +pub async fn create_additional_mint(rpc: &mut LightProgramTest, payer: &Keypair) -> Pubkey { + create_mint_helper(rpc, payer).await +} + /// Create destination account for testing account closure pub async fn setup_destination_account( rpc: &mut LightProgramTest, diff --git a/program-tests/compressed-token-test/tests/mint/burn.rs b/program-tests/compressed-token-test/tests/mint/burn.rs index 18b9511808..37a73a9447 100644 --- a/program-tests/compressed-token-test/tests/mint/burn.rs +++ b/program-tests/compressed-token-test/tests/mint/burn.rs @@ -39,19 +39,7 @@ async fn setup_burn_test(mint_amount: u64) -> BurnTestContext { // Derive Mint PDA let (mint_pda, _) = find_mint_address(&mint_seed.pubkey()); - // Step 1: Create Light Token ATA for owner first (needed before minting) - let ctoken_ata = derive_token_ata(&owner_keypair.pubkey(), &mint_pda); - - let create_ata_ix = - CreateAssociatedTokenAccount::new(payer.pubkey(), owner_keypair.pubkey(), mint_pda) - .instruction() - .unwrap(); - - rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) - .await - .unwrap(); - - // Step 2: Create compressed mint + Mint + mint tokens in one call + // Step 1: Create compressed mint + Mint FIRST (without minting yet) light_test_utils::actions::mint_action_comprehensive( &mut rpc, &mint_seed, @@ -60,10 +48,7 @@ async fn setup_burn_test(mint_amount: u64) -> BurnTestContext { Some(DecompressMintParams::default()), // Creates Mint false, // Don't compress and close vec![], // No compressed recipients - vec![Recipient { - recipient: owner_keypair.pubkey().into(), - amount: mint_amount, - }], // Mint to Light Token in same tx + vec![], // No ctoken recipients yet None, // No mint authority update None, // No freeze authority update Some( @@ -80,6 +65,38 @@ async fn setup_burn_test(mint_amount: u64) -> BurnTestContext { .await .unwrap(); + // Step 2: Create Light Token ATA for owner (after mint exists on-chain) + let ctoken_ata = derive_token_ata(&owner_keypair.pubkey(), &mint_pda); + + let create_ata_ix = + CreateAssociatedTokenAccount::new(payer.pubkey(), owner_keypair.pubkey(), mint_pda) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Step 3: Mint tokens to Light Token + light_test_utils::actions::mint_action_comprehensive( + &mut rpc, + &mint_seed, + &mint_authority, + &payer, + None, // Mint already decompressed + false, // Don't compress and close + vec![], // No compressed recipients + vec![Recipient { + recipient: owner_keypair.pubkey().into(), + amount: mint_amount, + }], // Mint to Light Token + None, // No mint authority update + None, // No freeze authority update + None, // No new mint + ) + .await + .unwrap(); + BurnTestContext { rpc, payer, diff --git a/program-tests/compressed-token-test/tests/mint/cmint_resize.rs b/program-tests/compressed-token-test/tests/mint/cmint_resize.rs index e153976cb6..64fae16e71 100644 --- a/program-tests/compressed-token-test/tests/mint/cmint_resize.rs +++ b/program-tests/compressed-token-test/tests/mint/cmint_resize.rs @@ -922,7 +922,24 @@ async fn test_decompress_with_mint_to_ctoken() { .await .unwrap(); - // 2. Create CToken ATA for recipient + // 2. Decompress mint FIRST (before creating CToken ATA) + mint_action_comprehensive( + &mut rpc, + &mint_seed, + &authority, + &payer, + Some(light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams::default()), + false, + vec![], + vec![], + None, + None, + None, + ) + .await + .unwrap(); + + // 3. Create CToken ATA for recipient (after mint exists on-chain) let recipient = Keypair::new(); let compressible_params = CompressibleParams { compressible_config: rpc @@ -947,30 +964,20 @@ async fn test_decompress_with_mint_to_ctoken() { .await .unwrap(); - // 3. Get pre-state - let compressed_account = rpc - .indexer() - .unwrap() - .get_compressed_account(compressed_mint_address, None) + // 4. Get pre-state from on-chain CMint + let cmint_account_data = rpc + .get_account(spl_mint_pda) .await .unwrap() - .value - .unwrap(); + .expect("CMint should exist"); let pre_mint: Mint = - BorshDeserialize::deserialize(&mut compressed_account.data.unwrap().data.as_slice()) - .unwrap(); + BorshDeserialize::deserialize(&mut cmint_account_data.data.as_slice()).unwrap(); - // 4. DecompressMint + MintToCToken - let actions = vec![ - MintActionType::DecompressMint { - rent_payment: 2, - write_top_up: 0, - }, - MintActionType::MintToCToken { - account: derive_token_ata(&recipient.pubkey(), &spl_mint_pda), - amount: 5000, - }, - ]; + // 5. MintToCToken (mint already decompressed) + let actions = vec![MintActionType::MintToCToken { + account: derive_token_ata(&recipient.pubkey(), &spl_mint_pda), + amount: 5000, + }]; mint_action( &mut rpc, @@ -989,7 +996,7 @@ async fn test_decompress_with_mint_to_ctoken() { .await .unwrap(); - // 5. Verify + // 6. Verify assert_mint_action(&mut rpc, compressed_mint_address, pre_mint, actions).await; } @@ -1043,7 +1050,24 @@ async fn test_decompress_with_all_operations() { .await .unwrap(); - // 2. Create CToken ATA for MintToCToken + // 2. Decompress mint FIRST (before creating CToken ATA) + mint_action_comprehensive( + &mut rpc, + &mint_seed, + &authority, + &payer, + Some(light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams::default()), + false, + vec![], + vec![], + None, + None, + None, + ) + .await + .unwrap(); + + // 3. Create CToken ATA for MintToCToken (after mint exists on-chain) let recipient = Keypair::new(); let compressible_params = CompressibleParams { compressible_config: rpc @@ -1068,31 +1092,22 @@ async fn test_decompress_with_all_operations() { .await .unwrap(); - // 3. Get pre-state from compressed account - let compressed_account = rpc - .indexer() - .unwrap() - .get_compressed_account(compressed_mint_address, None) + // 4. Get pre-state from on-chain CMint + let cmint_account_data = rpc + .get_account(spl_mint_pda) .await .unwrap() - .value - .unwrap(); + .expect("CMint should exist"); let pre_mint: Mint = - BorshDeserialize::deserialize(&mut compressed_account.data.unwrap().data.as_slice()) - .unwrap(); + BorshDeserialize::deserialize(&mut cmint_account_data.data.as_slice()).unwrap(); // New authorities let new_mint_authority = Keypair::new(); let new_freeze_authority = Keypair::new(); let new_metadata_authority = Keypair::new(); - // 4. DecompressMint + ALL other operations + // 5. ALL other operations (mint already decompressed) let actions = vec![ - // DecompressMint - MintActionType::DecompressMint { - rent_payment: 2, - write_top_up: 0, - }, // MintTo (compressed recipients) MintActionType::MintTo { recipients: vec![MintToRecipient { @@ -1165,6 +1180,6 @@ async fn test_decompress_with_all_operations() { .await .unwrap(); - // 5. Verify + // 6. Verify assert_mint_action(&mut rpc, compressed_mint_address, pre_mint, actions).await; } diff --git a/program-tests/compressed-token-test/tests/mint/edge_cases.rs b/program-tests/compressed-token-test/tests/mint/edge_cases.rs index 577c7a06d0..f228a8a02c 100644 --- a/program-tests/compressed-token-test/tests/mint/edge_cases.rs +++ b/program-tests/compressed-token-test/tests/mint/edge_cases.rs @@ -133,6 +133,23 @@ async fn functional_all_in_one_instruction() { ]), }), ); + + // Decompress mint so CToken ATA can be created + light_test_utils::actions::mint_action_comprehensive( + &mut rpc, + &mint_seed, + &authority, + &payer, + Some(light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams::default()), + false, + vec![], + vec![], + None, + None, + None, + ) + .await + .unwrap(); } // Fund authority @@ -145,7 +162,7 @@ async fn functional_all_in_one_instruction() { let new_freeze_authority = Keypair::new(); let new_metadata_authority = Keypair::new(); - // Create a compressible ctoken account for MintToCToken + // Create a compressible ctoken account for MintToCToken (after mint is decompressed) let recipient = Keypair::new(); let compressible_params = CompressibleParams { compressible_config: rpc @@ -234,20 +251,15 @@ async fn functional_all_in_one_instruction() { }, ]; - // Get pre-state compressed mint - let pre_compressed_mint_account = rpc - .indexer() - .unwrap() - .get_compressed_account(compressed_mint_address, None) + // Get pre-state from on-chain CMint (since mint was decompressed) + let cmint_account_data = rpc + .get_account(spl_mint_pda) .await .unwrap() - .value - .unwrap(); + .expect("CMint should exist after decompression"); - let pre_compressed_mint: Mint = BorshDeserialize::deserialize( - &mut pre_compressed_mint_account.data.unwrap().data.as_slice(), - ) - .unwrap(); + let pre_compressed_mint: Mint = + BorshDeserialize::deserialize(&mut cmint_account_data.data.as_slice()).unwrap(); // Execute all actions in a single instruction let result = light_test_utils::actions::mint_action( diff --git a/program-tests/compressed-token-test/tests/mint/failing.rs b/program-tests/compressed-token-test/tests/mint/failing.rs index e0ffb9f073..dfa398cc2e 100644 --- a/program-tests/compressed-token-test/tests/mint/failing.rs +++ b/program-tests/compressed-token-test/tests/mint/failing.rs @@ -402,6 +402,23 @@ async fn functional_and_failing_tests() { // 9. MintToCToken with invalid mint authority { + // Decompress mint first so CToken ATAs can be created + light_test_utils::actions::mint_action_comprehensive( + &mut rpc, + &mint_seed, + &new_mint_authority, // Use new_mint_authority since we updated it in step 6 + &payer, + Some(light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams::default()), + false, + vec![], + vec![], + None, + None, + None, + ) + .await + .unwrap(); + // Create a ctoken account first let recipient = Keypair::new(); @@ -443,19 +460,14 @@ async fn functional_and_failing_tests() { // 10. SUCCEED - MintToCToken with valid mint authority { - // Get pre-transaction compressed mint state - let pre_compressed_mint_account = rpc - .indexer() - .unwrap() - .get_compressed_account(compressed_mint_address, None) + // Get pre-transaction state from on-chain CMint (since mint was decompressed) + let cmint_account_data = rpc + .get_account(spl_mint_pda) .await .unwrap() - .value - .unwrap(); - let pre_compressed_mint: Mint = BorshDeserialize::deserialize( - &mut pre_compressed_mint_account.data.unwrap().data.as_slice(), - ) - .unwrap(); + .expect("CMint should exist after decompression"); + let pre_compressed_mint: Mint = + BorshDeserialize::deserialize(&mut cmint_account_data.data.as_slice()).unwrap(); // Create a new recipient for successful mint let recipient2 = Keypair::new(); @@ -539,19 +551,14 @@ async fn functional_and_failing_tests() { // 12. SUCCEED - UpdateMetadataField with valid metadata authority { - // Get pre-transaction compressed mint state - let pre_compressed_mint_account = rpc - .indexer() - .unwrap() - .get_compressed_account(compressed_mint_address, None) + // Get pre-transaction state from on-chain CMint (since mint was decompressed) + let cmint_account_data = rpc + .get_account(spl_mint_pda) .await .unwrap() - .value - .unwrap(); - let pre_compressed_mint: Mint = BorshDeserialize::deserialize( - &mut pre_compressed_mint_account.data.unwrap().data.as_slice(), - ) - .unwrap(); + .expect("CMint should exist"); + let pre_compressed_mint: Mint = + BorshDeserialize::deserialize(&mut cmint_account_data.data.as_slice()).unwrap(); let actions = vec![MintActionType::UpdateMetadataField { extension_index: 0, @@ -620,19 +627,14 @@ async fn functional_and_failing_tests() { // 14. SUCCEED - UpdateMetadataAuthority with valid metadata authority { - // Get pre-transaction compressed mint state - let pre_compressed_mint_account = rpc - .indexer() - .unwrap() - .get_compressed_account(compressed_mint_address, None) + // Get pre-transaction state from on-chain CMint (since mint was decompressed) + let cmint_account_data = rpc + .get_account(spl_mint_pda) .await .unwrap() - .value - .unwrap(); - let pre_compressed_mint: Mint = BorshDeserialize::deserialize( - &mut pre_compressed_mint_account.data.unwrap().data.as_slice(), - ) - .unwrap(); + .expect("CMint should exist"); + let pre_compressed_mint: Mint = + BorshDeserialize::deserialize(&mut cmint_account_data.data.as_slice()).unwrap(); let actions = vec![MintActionType::UpdateMetadataAuthority { extension_index: 0, @@ -700,19 +702,14 @@ async fn functional_and_failing_tests() { // 16. SUCCEED - RemoveMetadataKey with valid metadata authority { - // Get pre-transaction compressed mint state - let pre_compressed_mint_account = rpc - .indexer() - .unwrap() - .get_compressed_account(compressed_mint_address, None) + // Get pre-transaction state from on-chain CMint (since mint was decompressed) + let cmint_account_data = rpc + .get_account(spl_mint_pda) .await .unwrap() - .value - .unwrap(); - let pre_compressed_mint: Mint = BorshDeserialize::deserialize( - &mut pre_compressed_mint_account.data.unwrap().data.as_slice(), - ) - .unwrap(); + .expect("CMint should exist"); + let pre_compressed_mint: Mint = + BorshDeserialize::deserialize(&mut cmint_account_data.data.as_slice()).unwrap(); let actions = vec![MintActionType::RemoveMetadataKey { extension_index: 0, @@ -753,19 +750,14 @@ async fn functional_and_failing_tests() { // 17. SUCCEED - RemoveMetadataKey idempotent (try to remove same key again) { - // Get pre-transaction compressed mint state - let pre_compressed_mint_account = rpc - .indexer() - .unwrap() - .get_compressed_account(compressed_mint_address, None) + // Get pre-transaction state from on-chain CMint (since mint was decompressed) + let cmint_account_data = rpc + .get_account(spl_mint_pda) .await .unwrap() - .value - .unwrap(); - let pre_compressed_mint: Mint = BorshDeserialize::deserialize( - &mut pre_compressed_mint_account.data.unwrap().data.as_slice(), - ) - .unwrap(); + .expect("CMint should exist"); + let pre_compressed_mint: Mint = + BorshDeserialize::deserialize(&mut cmint_account_data.data.as_slice()).unwrap(); let actions = vec![MintActionType::RemoveMetadataKey { extension_index: 0, @@ -857,6 +849,23 @@ async fn test_mint_to_ctoken_max_top_up_exceeded() { .await .unwrap(); + // 1b. Decompress mint so CToken ATA can be created + light_test_utils::actions::mint_action_comprehensive( + &mut rpc, + &mint_seed, + &mint_authority, + &payer, + Some(light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams::default()), + false, + vec![], + vec![], + None, + None, + None, + ) + .await + .unwrap(); + // 2. Create compressible Light Token ATA with pre_pay_num_epochs = 0 (NO prepaid rent) let recipient = Keypair::new(); @@ -886,7 +895,17 @@ async fn test_mint_to_ctoken_max_top_up_exceeded() { let ctoken_ata = light_token::instruction::derive_token_ata(&recipient.pubkey(), &spl_mint_pda); // 3. Build MintToCToken instruction with max_top_up = 1 (too low) - // Get current compressed mint state + // Get current mint state from on-chain CMint (since mint was decompressed) + let cmint_account_data = rpc + .get_account(spl_mint_pda) + .await + .unwrap() + .expect("CMint should exist after decompression"); + + let compressed_mint: light_token_interface::state::Mint = + BorshDeserialize::deserialize(&mut cmint_account_data.data.as_slice()).unwrap(); + + // Get compressed account for proof (still exists but with mint_decompressed=true) let compressed_mint_account = rpc .indexer() .unwrap() @@ -896,10 +915,6 @@ async fn test_mint_to_ctoken_max_top_up_exceeded() { .value .unwrap(); - let compressed_mint: light_token_interface::state::Mint = - BorshDeserialize::deserialize(&mut compressed_mint_account.data.unwrap().data.as_slice()) - .unwrap(); - // Get validity proof let rpc_proof_result = rpc .get_validity_proof(vec![compressed_mint_account.hash], vec![], None) diff --git a/program-tests/compressed-token-test/tests/mint/functional.rs b/program-tests/compressed-token-test/tests/mint/functional.rs index 1695ecaa88..dc9e7a5913 100644 --- a/program-tests/compressed-token-test/tests/mint/functional.rs +++ b/program-tests/compressed-token-test/tests/mint/functional.rs @@ -215,7 +215,24 @@ async fn test_create_compressed_mint() { let decompress_amount = 300u64; - // 5. Decompress compressed tokens to ctokens + // 5. Decompress the mint first (required before creating CToken ATA) + light_test_utils::actions::mint_action_comprehensive( + &mut rpc, + &mint_seed, + &mint_authority_keypair, + &payer, + Some(DecompressMintParams::default()), // Create on-chain Mint + false, + vec![], + vec![], + None, + None, + None, // No new mint - already exists as compressed + ) + .await + .unwrap(); + + // 6. Decompress compressed tokens to ctokens // Create non-compressible token associated token account for decompression let ctoken_ata_pubkey = derive_token_ata(&new_recipient, &spl_mint_pda); let create_ata_instruction = CreateAssociatedTokenAccount { @@ -685,7 +702,33 @@ async fn test_ctoken_transfer() { // Derive addresses let (spl_mint_pda, _) = find_mint_address(&mint_seed.pubkey()); - // Create compressed token ATA for recipient + // === STEP 1: CREATE COMPRESSED MINT + DECOMPRESS FIRST === + light_test_utils::actions::mint_action_comprehensive( + &mut rpc, + &mint_seed, + &mint_authority, + &payer, + Some(DecompressMintParams::default()), // decompress_mint - creates on-chain Mint + false, // compress_and_close_mint + vec![], // no compressed recipients + vec![], // no decompressed recipients yet + None, // no mint authority update + None, // no freeze authority update + Some( + light_test_utils::actions::legacy::instructions::mint_action::NewMint { + decimals, + supply: 0, + mint_authority: mint_authority.pubkey(), + freeze_authority: Some(freeze_authority.pubkey()), + metadata: None, // No metadata for simplicity + version: 3, + }, + ), + ) + .await + .unwrap(); + + // === STEP 2: CREATE COMPRESSED TOKEN ATA (after mint exists on-chain) === let recipient_ata = derive_token_ata(&recipient_keypair.pubkey(), &spl_mint_pda); let compressible_params = CompressibleParams { compressible_config: rpc @@ -708,10 +751,8 @@ async fn test_ctoken_transfer() { rpc.create_and_send_transaction(&[create_ata_instruction], &payer.pubkey(), &[&payer]) .await .unwrap(); - // rpc.airdrop_lamports(&recipient_ata, 10_000_000_090) - // .await - // .unwrap(); - // === STEP 1: CREATE COMPRESSED MINT AND MINT TO DECOMPRESSED ACCOUNT === + + // === STEP 3: MINT TO DECOMPRESSED ACCOUNT === let decompressed_recipients = vec![Recipient { recipient: recipient_keypair.pubkey().to_bytes().into(), amount: 100000000u64, @@ -722,22 +763,13 @@ async fn test_ctoken_transfer() { &mint_seed, &mint_authority, &payer, - None, // decompress_mint + None, // decompress_mint - already decompressed false, // compress_and_close_mint vec![], // no compressed recipients decompressed_recipients, // mint to decompressed recipients None, // no mint authority update None, // no freeze authority update - Some( - light_test_utils::actions::legacy::instructions::mint_action::NewMint { - decimals, - supply: 0, - mint_authority: mint_authority.pubkey(), - freeze_authority: Some(freeze_authority.pubkey()), - metadata: None, // No metadata for simplicity - version: 3, - }, - ), + None, // no new mint - already exists ) .await .unwrap(); diff --git a/program-tests/compressed-token-test/tests/mint/mint_to.rs b/program-tests/compressed-token-test/tests/mint/mint_to.rs index 855fdbac79..88efbc2afd 100644 --- a/program-tests/compressed-token-test/tests/mint/mint_to.rs +++ b/program-tests/compressed-token-test/tests/mint/mint_to.rs @@ -37,19 +37,7 @@ async fn setup_mint_to_test() -> MintToTestContext { // Derive Mint PDA let (mint_pda, _) = find_mint_address(&mint_seed.pubkey()); - // Step 1: Create Light Token ATA for owner first - let ctoken_ata = derive_token_ata(&owner_keypair.pubkey(), &mint_pda); - - let create_ata_ix = - CreateAssociatedTokenAccount::new(payer.pubkey(), owner_keypair.pubkey(), mint_pda) - .instruction() - .unwrap(); - - rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) - .await - .unwrap(); - - // Step 2: Create compressed mint + Mint (no recipients - we'll mint via MintTo) + // Step 1: Create compressed mint + Mint FIRST (no recipients - we'll mint via MintTo) light_test_utils::actions::mint_action_comprehensive( &mut rpc, &mint_seed, @@ -75,6 +63,18 @@ async fn setup_mint_to_test() -> MintToTestContext { .await .unwrap(); + // Step 2: Create Light Token ATA for owner (after mint exists on-chain) + let ctoken_ata = derive_token_ata(&owner_keypair.pubkey(), &mint_pda); + + let create_ata_ix = + CreateAssociatedTokenAccount::new(payer.pubkey(), owner_keypair.pubkey(), mint_pda) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + MintToTestContext { rpc, payer, diff --git a/program-tests/compressed-token-test/tests/mint/random.rs b/program-tests/compressed-token-test/tests/mint/random.rs index 534e513e4b..c4c5d2430d 100644 --- a/program-tests/compressed-token-test/tests/mint/random.rs +++ b/program-tests/compressed-token-test/tests/mint/random.rs @@ -124,6 +124,23 @@ async fn test_random_mint_action() { }), ); + // Decompress mint so CToken ATAs can be created + light_test_utils::actions::mint_action_comprehensive( + &mut rpc, + &mint_seed, + &authority, + &payer, + Some(light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams::default()), + false, + vec![], + vec![], + None, + None, + None, + ) + .await + .unwrap(); + // Fund authority rpc.airdrop_lamports(&authority.pubkey(), 10_000_000_000) .await @@ -336,20 +353,15 @@ async fn test_random_mint_action() { } } - // Get pre-state compressed mint - let pre_compressed_mint_account = rpc - .indexer() - .unwrap() - .get_compressed_account(compressed_mint_address, None) + // Get pre-state from on-chain CMint (since mint was decompressed) + let cmint_account_data = rpc + .get_account(spl_mint_pda) .await .unwrap() - .value - .unwrap(); + .expect("CMint should exist after decompression"); - let pre_compressed_mint: Mint = BorshDeserialize::deserialize( - &mut pre_compressed_mint_account.data.unwrap().data.as_slice(), - ) - .unwrap(); + let pre_compressed_mint: Mint = + BorshDeserialize::deserialize(&mut cmint_account_data.data.as_slice()).unwrap(); println!("actions {:?}", actions); // Execute all actions in a single instruction let result = light_test_utils::actions::mint_action( diff --git a/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs b/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs index 9cf8114e7c..19645fac72 100644 --- a/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs +++ b/program-tests/compressed-token-test/tests/transfer2/compress_failing.rs @@ -91,7 +91,34 @@ async fn setup_compression_test(token_amount: u64) -> Result Result Result<(), RpcError> { let (mint, _) = find_mint_address(&mint_seed.pubkey()); let ctoken_ata = derive_token_ata(&owner.pubkey(), &mint); + // First create AND decompress the mint (CToken ATA creation requires mint to exist on-chain) + light_test_utils::actions::mint_action_comprehensive( + &mut rpc, + &mint_seed, + &mint_authority, + &payer, + Some( + light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams::default(), + ), // decompress mint so it exists on-chain + false, // no close mint + vec![], // no compressed recipients + vec![], // no decompressed recipients yet + None, + None, + Some( + light_test_utils::actions::legacy::instructions::mint_action::NewMint { + decimals: 6, + supply: 0, + mint_authority: mint_authority.pubkey(), + freeze_authority: None, + metadata: None, + version: 3, // ShaFlat for compressible accounts + }, + ), + ) + .await?; + // Create compressible Light Token ATA with pre_pay_num_epochs = 0 (NO prepaid rent) // This means any write operation will require immediate rent top-up let compressible_params = CompressibleParams { @@ -626,31 +669,20 @@ async fn test_compression_max_top_up_exceeded() -> Result<(), RpcError> { rpc.create_and_send_transaction(&[create_ata_instruction], &payer.pubkey(), &[&payer]) .await?; - // Create mint and mint tokens to decompressed Light Token ATA + // Mint tokens to the Light Token ATA let token_amount = 1000u64; - let decompressed_recipients = vec![Recipient::new(owner.pubkey(), token_amount)]; - light_test_utils::actions::mint_action_comprehensive( &mut rpc, &mint_seed, &mint_authority, &payer, - None, // no decompress mint - false, // no close mint - vec![], // no compressed recipients - decompressed_recipients, // mint to decompressed Light Token ATA + None, // mint already decompressed + false, // no close mint + vec![], // no compressed recipients + vec![Recipient::new(owner.pubkey(), token_amount)], // mint to Light Token ATA None, None, - Some( - light_test_utils::actions::legacy::instructions::mint_action::NewMint { - decimals: 6, - supply: 0, - mint_authority: mint_authority.pubkey(), - freeze_authority: None, - metadata: None, - version: 3, // ShaFlat for compressible accounts - }, - ), + None, // mint already exists ) .await?; @@ -734,6 +766,33 @@ async fn test_compression_duplicate_account_no_double_charge_top_up() -> Result< let (mint, _) = find_mint_address(&mint_seed.pubkey()); let ctoken_ata = derive_token_ata(&owner.pubkey(), &mint); + // First create AND decompress the mint (ATA creation requires mint to exist on-chain) + light_test_utils::actions::mint_action_comprehensive( + &mut rpc, + &mint_seed, + &mint_authority, + &payer, + Some( + light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams::default(), + ), // decompress mint so it exists on-chain + false, // no close mint + vec![], // no compressed recipients + vec![], // no decompressed recipients yet + None, + None, + Some( + light_test_utils::actions::legacy::instructions::mint_action::NewMint { + decimals: 6, + supply: 0, + mint_authority: mint_authority.pubkey(), + freeze_authority: None, + metadata: None, + version: 3, // ShaFlat for compressible accounts + }, + ), + ) + .await?; + // Create compressible Light Token ATA with pre_pay_num_epochs = 0 (NO prepaid rent) let compressible_params = CompressibleParams { compressible_config: rpc @@ -766,22 +825,13 @@ async fn test_compression_duplicate_account_no_double_charge_top_up() -> Result< &mint_seed, &mint_authority, &payer, - None, + None, // mint already decompressed false, vec![], decompressed_recipients, None, None, - Some( - light_test_utils::actions::legacy::instructions::mint_action::NewMint { - decimals: 6, - supply: 0, - mint_authority: mint_authority.pubkey(), - freeze_authority: None, - metadata: None, - version: 3, - }, - ), + None, // mint already exists ) .await?; diff --git a/program-tests/compressed-token-test/tests/transfer2/decompress_failing.rs b/program-tests/compressed-token-test/tests/transfer2/decompress_failing.rs index fe23a0348f..1490e57197 100644 --- a/program-tests/compressed-token-test/tests/transfer2/decompress_failing.rs +++ b/program-tests/compressed-token-test/tests/transfer2/decompress_failing.rs @@ -91,42 +91,21 @@ async fn setup_decompression_test( let (mint, _) = find_mint_address(&mint_seed.pubkey()); let ctoken_ata = derive_token_ata(&owner.pubkey(), &mint); - // Create compressible Light Token ATA for owner (recipient of decompression) - let compressible_params = CompressibleParams { - compressible_config: rpc - .test_accounts - .funding_pool_config - .compressible_config_pda, - rent_sponsor: rpc.test_accounts.funding_pool_config.rent_sponsor_pda, - pre_pay_num_epochs: 2, - lamports_per_write: Some(1000), - compress_to_account_pubkey: None, - token_account_version: TokenDataVersion::ShaFlat, - compression_only: true, // ATAs require compression_only=true - }; - - let create_ata_instruction = - CreateAssociatedTokenAccount::new(payer.pubkey(), owner.pubkey(), mint) - .with_compressible(compressible_params) - .instruction() - .map_err(|e| RpcError::AssertRpcError(format!("Failed to create ATA: {:?}", e)))?; - - rpc.create_and_send_transaction(&[create_ata_instruction], &payer.pubkey(), &[&payer]) - .await?; - - // Mint compressed tokens to owner and 1 token to decompressed Light Token ATA + // First create AND decompress the mint (CToken ATA creation requires mint to exist on-chain) + // Also mint compressed tokens to owner in the same call let compressed_recipients = vec![Recipient::new(owner.pubkey(), compressed_amount)]; - let decompressed_recipients = vec![Recipient::new(owner.pubkey(), 0)]; light_test_utils::actions::mint_action_comprehensive( &mut rpc, &mint_seed, &mint_authority, &payer, - None, // no decompress mint + Some( + light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams::default(), + ), // decompress mint so it exists on-chain false, // compress_and_close_mint compressed_recipients, // mint compressed tokens to owner - decompressed_recipients, // mint 1 token to decompressed Light Token ATA + vec![], // no decompressed recipients yet (ATA doesn't exist) None, // no mint authority update None, // no freeze authority update Some( @@ -142,6 +121,29 @@ async fn setup_decompression_test( ) .await?; + // Create compressible Light Token ATA for owner (recipient of decompression) - now mint exists on-chain + let compressible_params = CompressibleParams { + compressible_config: rpc + .test_accounts + .funding_pool_config + .compressible_config_pda, + rent_sponsor: rpc.test_accounts.funding_pool_config.rent_sponsor_pda, + pre_pay_num_epochs: 2, + lamports_per_write: Some(1000), + compress_to_account_pubkey: None, + token_account_version: TokenDataVersion::ShaFlat, + compression_only: true, // ATAs require compression_only=true + }; + + let create_ata_instruction = + CreateAssociatedTokenAccount::new(payer.pubkey(), owner.pubkey(), mint) + .with_compressible(compressible_params) + .instruction() + .map_err(|e| RpcError::AssertRpcError(format!("Failed to create ATA: {:?}", e)))?; + + rpc.create_and_send_transaction(&[create_ata_instruction], &payer.pubkey(), &[&payer]) + .await?; + // Get compressed token account from indexer let compressed_token_accounts = rpc .indexer() diff --git a/program-tests/compressed-token-test/tests/transfer2/no_system_program_cpi_failing.rs b/program-tests/compressed-token-test/tests/transfer2/no_system_program_cpi_failing.rs index 7cbda9c766..38e040e90f 100644 --- a/program-tests/compressed-token-test/tests/transfer2/no_system_program_cpi_failing.rs +++ b/program-tests/compressed-token-test/tests/transfer2/no_system_program_cpi_failing.rs @@ -105,40 +105,18 @@ async fn setup_no_system_program_cpi_test( let source_ata = derive_token_ata(&owner.pubkey(), &mint); let recipient_ata = derive_token_ata(&recipient.pubkey(), &mint); - // Create Light Token ATA for owner (source) - let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), owner.pubkey(), mint) - .instruction() - .map_err(|e| RpcError::AssertRpcError(format!("Failed to create source ATA: {}", e))) - .unwrap(); - rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) - .await - .unwrap(); - - // Create Light Token ATA for recipient - let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), recipient.pubkey(), mint) - .instruction() - .map_err(|e| RpcError::AssertRpcError(format!("Failed to create recipient ATA: {}", e))) - .unwrap(); - rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) - .await - .unwrap(); - - // Create mint and mint tokens to source Light Token ATA - let decompressed_recipients = if source_token_amount > 0 { - vec![Recipient::new(owner.pubkey(), source_token_amount)] - } else { - vec![] - }; - + // First create AND decompress the mint (CToken ATA creation requires mint to exist on-chain) light_test_utils::actions::mint_action_comprehensive( &mut rpc, &mint_seed, &mint_authority, &payer, - None, // no decompress mint + Some( + light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams::default(), + ), // decompress mint so it exists on-chain false, // no close mint vec![], // no compressed recipients - decompressed_recipients, // mint to source Light Token ATA (empty if token_amount is 0) + vec![], // no decompressed recipients yet None, None, Some( @@ -155,6 +133,43 @@ async fn setup_no_system_program_cpi_test( .await .unwrap(); + // Create Light Token ATA for owner (source) - now mint exists on-chain + let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), owner.pubkey(), mint) + .instruction() + .map_err(|e| RpcError::AssertRpcError(format!("Failed to create source ATA: {}", e))) + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Create Light Token ATA for recipient + let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), recipient.pubkey(), mint) + .instruction() + .map_err(|e| RpcError::AssertRpcError(format!("Failed to create recipient ATA: {}", e))) + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Mint tokens to source Light Token ATA if needed + if source_token_amount > 0 { + light_test_utils::actions::mint_action_comprehensive( + &mut rpc, + &mint_seed, + &mint_authority, + &payer, + None, // mint already decompressed + false, // no close mint + vec![], // no compressed recipients + vec![Recipient::new(owner.pubkey(), source_token_amount)], // mint to source ATA + None, + None, + None, // mint already exists + ) + .await + .unwrap(); + } + // Build compressions and packed accounts for default balanced case (500 compress, 500 decompress) let (compressions, packed_accounts) = create_compressions_and_packed_accounts( mint, @@ -712,7 +727,35 @@ async fn test_too_many_mints() { let source_ata = derive_token_ata(&context.owner.pubkey(), &mint); let recipient_ata = derive_token_ata(&context.recipient.pubkey(), &mint); - // Create source ATA + // First create AND decompress the mint (CToken ATA creation requires mint to exist on-chain) + light_test_utils::actions::mint_action_comprehensive( + &mut context.rpc, + &mint_seed, + &mint_authority, + &context.payer, + Some( + light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams::default(), + ), // decompress mint so it exists on-chain + false, // no close mint + vec![], // no compressed recipients + vec![], // no decompressed recipients yet + None, + None, + Some( + light_test_utils::actions::legacy::instructions::mint_action::NewMint { + decimals: 6, + supply: 0, + mint_authority: mint_authority.pubkey(), + freeze_authority: None, + metadata: None, + version: 3, // ShaFlat + }, + ), + ) + .await + .unwrap(); + + // Create source ATA - now mint exists on-chain let instruction = CreateAssociatedTokenAccount::new(context.payer.pubkey(), context.owner.pubkey(), mint) .instruction() @@ -737,30 +780,19 @@ async fn test_too_many_mints() { .await .unwrap(); - // Create mint and mint tokens to source Light Token ATA - let decompressed_recipients = vec![Recipient::new(context.owner.pubkey(), 1000)]; - + // Mint tokens to source Light Token ATA light_test_utils::actions::mint_action_comprehensive( &mut context.rpc, &mint_seed, &mint_authority, &context.payer, - None, // no decompress mint - false, // no close mint - vec![], // no compressed recipients - decompressed_recipients, // mint to source Light Token ATA + None, // mint already decompressed + false, // no close mint + vec![], // no compressed recipients + vec![Recipient::new(context.owner.pubkey(), 1000)], // mint to source ATA None, None, - Some( - light_test_utils::actions::legacy::instructions::mint_action::NewMint { - decimals: 6, - supply: 0, - mint_authority: mint_authority.pubkey(), - freeze_authority: None, - metadata: None, - version: 3, // ShaFlat - }, - ), + None, // mint already exists ) .await .unwrap(); diff --git a/program-tests/compressed-token-test/tests/transfer2/shared.rs b/program-tests/compressed-token-test/tests/transfer2/shared.rs index 0596494a1f..d692b49517 100644 --- a/program-tests/compressed-token-test/tests/transfer2/shared.rs +++ b/program-tests/compressed-token-test/tests/transfer2/shared.rs @@ -10,13 +10,13 @@ use light_test_utils::{ actions::{ create_mint, legacy::instructions::{ - mint_action::MintActionType, + mint_action::{DecompressMintParams, MintActionType}, transfer2::{ create_generic_transfer2_instruction, ApproveInput, CompressAndCloseInput, CompressInput, DecompressInput, Transfer2InstructionType, TransferInput, }, }, - mint_to_compressed, + mint_action_comprehensive, mint_to_compressed, }, airdrop_lamports, assert_transfer2::assert_transfer2, @@ -430,6 +430,40 @@ impl TestContext { // Get compressible config from test accounts (already created in program test setup) let funding_pool_config = rpc.test_accounts.funding_pool_config; + // Decompress compressed mints that will be used for CToken ATAs + // CToken ATA creation requires the Mint account to exist on-chain + let mut decompressed_mints: std::collections::HashSet = + std::collections::HashSet::new(); + for (_, mint_index) in requirements.signer_ctoken_amounts.keys() { + // Only decompress compressed mints (not SPL mints) + if !mint_needs_spl[*mint_index] && !decompressed_mints.contains(mint_index) { + let mint_seed = &mint_seeds[*mint_index]; + let mint_authority = &mint_authorities[*mint_index]; + + println!( + "Decompressing compressed mint {} for CToken ATA creation", + mint_index + ); + + mint_action_comprehensive( + &mut rpc, + mint_seed, + mint_authority, + &payer, + Some(DecompressMintParams::default()), + false, // compress_and_close_mint + vec![], // mint_to_recipients + vec![], // mint_to_decompressed_recipients + None, // update_mint_authority + None, // update_freeze_authority + None, // new_mint + ) + .await?; + + decompressed_mints.insert(*mint_index); + } + } + // Create Light Token ATAs for compress/decompress operations let mut ctoken_atas = HashMap::new(); for ((signer_index, mint_index), &amount) in &requirements.signer_ctoken_amounts { diff --git a/program-tests/registry-test/tests/compressible.rs b/program-tests/registry-test/tests/compressible.rs index ee65152021..645e40c468 100644 --- a/program-tests/registry-test/tests/compressible.rs +++ b/program-tests/registry-test/tests/compressible.rs @@ -107,7 +107,7 @@ async fn test_claim_rent_for_completed_epochs() -> Result<(), RpcError> { .unwrap(); let payer = rpc.get_payer().insecure_clone(); let _payer_pubkey = payer.pubkey(); - let mint = Pubkey::new_unique(); + let mint = create_mint_helper(&mut rpc, &payer).await; let compressible_owner_keypair = Keypair::new(); let compressible_owner_pubkey = compressible_owner_keypair.pubkey(); @@ -479,6 +479,9 @@ async fn test_pause_compressible_config_with_valid_authority() -> Result<(), Rpc .unwrap(); let payer = rpc.get_payer().insecure_clone(); + // Create mint before pausing (required for token account creation) + let mint = create_mint_helper(&mut rpc, &payer).await; + // Pause the config with valid authority pause_compressible_config(&mut rpc, &payer, &payer) .await @@ -499,7 +502,7 @@ async fn test_pause_compressible_config_with_valid_authority() -> Result<(), Rpc // Test 1: Cannot create new token accounts with paused config let compressible_instruction = - CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), Pubkey::new_unique()) + CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), mint) .with_compressible(CompressibleParams::default_ata()) .instruction() .map_err(|e| { @@ -602,6 +605,9 @@ async fn test_unpause_compressible_config_with_valid_authority() -> Result<(), R .unwrap(); let payer = rpc.get_payer().insecure_clone(); + // Create mint before pausing (required for token account creation) + let mint = create_mint_helper(&mut rpc, &payer).await; + // First pause the config pause_compressible_config(&mut rpc, &payer, &payer) .await @@ -620,7 +626,7 @@ async fn test_unpause_compressible_config_with_valid_authority() -> Result<(), R // Verify cannot create account while paused let compressible_instruction = - CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), Pubkey::new_unique()) + CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), mint) .with_compressible(CompressibleParams::default_ata()) .instruction() .map_err(|e| { @@ -667,7 +673,7 @@ async fn test_unpause_compressible_config_with_valid_authority() -> Result<(), R }; let compressible_instruction = - CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), Pubkey::new_unique()) + CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), mint) .with_compressible(compressible_params) .instruction() .map_err(|e| { @@ -741,7 +747,7 @@ async fn test_deprecate_compressible_config_with_valid_authority() -> Result<(), // First create a compressible account while config is active let token_account_keypair = Keypair::new(); - let mint = Pubkey::new_unique(); + let mint = create_mint_helper(&mut rpc, &payer).await; let compressible_params = CompressibleParams { compressible_config: rpc @@ -1597,7 +1603,7 @@ async fn test_claim_mixed_token_and_mint() -> Result<(), RpcError> { // Create Light Token account with prepaid rent let token_owner = Keypair::new(); - let mint = Pubkey::new_unique(); + let mint = create_mint_helper(&mut rpc, &payer).await; let token_pubkey = create_compressible_token_account( &mut rpc, CreateCompressibleTokenAccountInputs { diff --git a/program-tests/utils/src/assert_create_token_account.rs b/program-tests/utils/src/assert_create_token_account.rs index 0e5912c040..6f799b3d0f 100644 --- a/program-tests/utils/src/assert_create_token_account.rs +++ b/program-tests/utils/src/assert_create_token_account.rs @@ -48,7 +48,21 @@ async fn get_expected_extensions_from_mint( // Check if this is a Token-2022 mint (program owner) if mint_account.owner != spl_token_2022::ID { - // Regular SPL Token mint - no extensions, not compression_only + // Regular SPL Token mint - read decimals, but no extensions + // SPL Token mint layout: supply (8) + decimals (1) + ... + use anchor_spl::token::spl_token; + use solana_sdk::program_pack::Pack; + if mint_account.owner == spl_token::ID { + let mint_state = spl_token::state::Mint::unpack(&mint_account.data) + .expect("Failed to unpack SPL Token mint"); + return ( + Some(mint_state.decimals), + AccountState::Initialized, + None, + false, + ); + } + // Unknown mint program - no extensions, not compression_only return (None, AccountState::Initialized, None, false); } diff --git a/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs b/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs index 1a19e89a76..837c60a24b 100644 --- a/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs +++ b/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs @@ -167,6 +167,22 @@ pub fn initialize_ctoken_account( mint_account, } = config; + // Validate mint account and extract decimals for all token accounts + let mint_decimals = { + let mint_data = AccountInfoTrait::try_borrow_data(mint_account)?; + if mint_data.is_empty() { + msg!("Invalid mint account: account data is empty"); + return Err(ProgramError::InvalidAccountData); + } + if !is_valid_mint(mint_account.owner(), &mint_data)? { + msg!("Invalid mint account: not a valid mint"); + return Err(ProgramError::InvalidAccountData); + } + // Mint layout: decimals at byte 44 for all token programs + // (mint_authority option: 36, supply: 8) = 44 + mint_data.get(44).copied() + }; + // Build extensions Vec from boolean flags // +1 for potential Compressible extension let mut extensions = Vec::with_capacity(mint_extensions.num_token_account_extensions() + 1); @@ -233,7 +249,7 @@ pub fn initialize_ctoken_account( // We need to re-read using zero_copy_at_mut because new_zero_copy doesn't // populate the extensions field (it only writes them to bytes) if let Some(compressible) = compressible { - configure_compression_info(&mut ctoken, compressible, mint_account)?; + configure_compression_info(&mut ctoken, compressible, mint_decimals)?; } Ok(()) @@ -244,7 +260,7 @@ pub fn initialize_ctoken_account( fn configure_compression_info( ctoken: &mut light_token_interface::state::ZTokenMut<'_>, compressible: CompressibleInitData<'_>, - mint_account: &AccountInfo, + mint_decimals: Option, ) -> Result<(), ProgramError> { let CompressibleInitData { ix_data, @@ -325,21 +341,8 @@ fn configure_compression_info( } compressible_ext.info.account_version = ix_data.token_account_version; - // Read decimals from mint account and cache in extension - let mint_data = AccountInfoTrait::try_borrow_data(mint_account)?; - // Only try to read decimals if mint has data (is initialized) - if !mint_data.is_empty() { - let owner = mint_account.owner(); - - if !is_valid_mint(owner, &mint_data)? { - msg!("Invalid mint account: not a valid mint"); - return Err(ProgramError::InvalidAccountData); - } - - // Mint layout: decimals at byte 44 for all token programs - // (mint_authority option: 36, supply: 8) = 44 - compressible_ext.set_decimals(mint_data.get(44).copied()); - } + // Cache mint decimals in extension (already extracted during validation) + compressible_ext.set_decimals(mint_decimals); Ok(()) } diff --git a/sdk-libs/client/src/interface/load_accounts.rs b/sdk-libs/client/src/interface/load_accounts.rs index a73ecafa9f..061ad5074b 100644 --- a/sdk-libs/client/src/interface/load_accounts.rs +++ b/sdk-libs/client/src/interface/load_accounts.rs @@ -129,7 +129,12 @@ where let mut out = Vec::new(); - // 1. DecompressAccountsIdempotent for all cold PDAs (including token PDAs). + // 1. Mint loads first - ATAs require the mint to exist on-chain + for (iface, proof) in cold_mints.iter().zip(mint_proofs) { + out.push(build_mint_load(iface, proof, fee_payer)?); + } + + // 2. DecompressAccountsIdempotent for all cold PDAs (including token PDAs). // Token PDAs are created on-chain via CPI inside DecompressVariant. for (spec, proof) in cold_pdas.iter().zip(pda_proofs) { out.push(build_pda_load( @@ -140,16 +145,12 @@ where )?); } - // 2. ATA loads (CreateAssociatedTokenAccount + Transfer2) + // 3. ATA loads (CreateAssociatedTokenAccount + Transfer2) - requires mint to exist let ata_chunks: Vec<_> = cold_atas.chunks(MAX_ATAS_PER_IX).collect(); for (chunk, proof) in ata_chunks.into_iter().zip(ata_proofs) { out.extend(build_ata_load(chunk, proof, fee_payer)?); } - // 3. Mint loads - for (iface, proof) in cold_mints.iter().zip(mint_proofs) { - out.push(build_mint_load(iface, proof, fee_payer)?); - } Ok(out) } diff --git a/sdk-tests/anchor-semi-manual-test/tests/stress_test.rs b/sdk-tests/anchor-semi-manual-test/tests/stress_test.rs index 340726eb6e..8804f890be 100644 --- a/sdk-tests/anchor-semi-manual-test/tests/stress_test.rs +++ b/sdk-tests/anchor-semi-manual-test/tests/stress_test.rs @@ -314,6 +314,28 @@ async fn decompress_all(ctx: &mut StressTestContext, pdas: &TestPdas, cached: &C }; let vault_spec = PdaSpec::new(vault_interface, vault_variant, ctx.program_id); + // ATA Mint (pre-existing light mint that also gets compressed) + let ata_mint_iface = ctx + .rpc + .get_mint_interface(&pdas.ata_mint, None) + .await + .expect("failed to get ATA mint interface") + .value + .expect("ATA mint interface should exist"); + assert!(ata_mint_iface.is_cold(), "ATA mint should be cold"); + let ata_mint_ai = AccountInterface::from(ata_mint_iface); + + // Vault Mint (pre-existing light mint that also gets compressed) + let vault_mint_iface = ctx + .rpc + .get_mint_interface(&pdas.vault_mint, None) + .await + .expect("failed to get vault mint interface") + .value + .expect("vault mint interface should exist"); + assert!(vault_mint_iface.is_cold(), "Vault mint should be cold"); + let vault_mint_ai = AccountInterface::from(vault_mint_iface); + // Mint A let mint_a_iface = ctx .rpc @@ -336,10 +358,13 @@ async fn decompress_all(ctx: &mut StressTestContext, pdas: &TestPdas, cached: &C assert!(mint_b_iface.is_cold(), "Mint B should be cold"); let mint_b_ai = AccountInterface::from(mint_b_iface); + // Mints must come before ATA and vault since they depend on mints being decompressed let specs: Vec> = vec![ AccountSpec::Pda(record_spec), AccountSpec::Pda(zc_spec), + AccountSpec::Mint(ata_mint_ai), AccountSpec::Ata(Box::new(ata_interface)), + AccountSpec::Mint(vault_mint_ai), AccountSpec::Pda(vault_spec), AccountSpec::Mint(mint_a_ai), AccountSpec::Mint(mint_b_ai), @@ -359,7 +384,9 @@ async fn decompress_all(ctx: &mut StressTestContext, pdas: &TestPdas, cached: &C for (pda, name) in [ (&pdas.record, "MinimalRecord"), (&pdas.zc_record, "ZeroCopyRecord"), + (&pdas.ata_mint, "AtaMint"), (&pdas.ata, "ATA"), + (&pdas.vault_mint, "VaultMint"), (&pdas.vault, "Vault"), (&pdas.mint_a, "MintA"), (&pdas.mint_b, "MintB"), @@ -378,7 +405,9 @@ async fn compress_all(ctx: &mut StressTestContext, pdas: &TestPdas) { for (pda, name) in [ (&pdas.record, "MinimalRecord"), (&pdas.zc_record, "ZeroCopyRecord"), + (&pdas.ata_mint, "AtaMint"), (&pdas.ata, "ATA"), + (&pdas.vault_mint, "VaultMint"), (&pdas.vault, "Vault"), (&pdas.mint_a, "MintA"), (&pdas.mint_b, "MintB"), @@ -469,7 +498,9 @@ async fn test_stress_20_iterations() { for (pda, name) in [ (&pdas.record, "MinimalRecord"), (&pdas.zc_record, "ZeroCopyRecord"), + (&pdas.ata_mint, "AtaMint"), (&pdas.ata, "ATA"), + (&pdas.vault_mint, "VaultMint"), (&pdas.vault, "Vault"), (&pdas.mint_a, "MintA"), (&pdas.mint_b, "MintB"), diff --git a/sdk-tests/anchor-semi-manual-test/tests/test_create_all.rs b/sdk-tests/anchor-semi-manual-test/tests/test_create_all.rs index b4c91db9c4..3f5028ec3f 100644 --- a/sdk-tests/anchor-semi-manual-test/tests/test_create_all.rs +++ b/sdk-tests/anchor-semi-manual-test/tests/test_create_all.rs @@ -209,7 +209,9 @@ async fn test_create_all_derive() { shared::assert_onchain_closed(&mut rpc, &record_pda, "MinimalRecord").await; shared::assert_onchain_closed(&mut rpc, &zc_record_pda, "ZeroCopyRecord").await; + shared::assert_onchain_closed(&mut rpc, &ata_mint, "AtaMint").await; shared::assert_onchain_closed(&mut rpc, &ata, "ATA").await; + shared::assert_onchain_closed(&mut rpc, &vault_mint, "VaultMint").await; shared::assert_onchain_closed(&mut rpc, &vault, "Vault").await; shared::assert_onchain_closed(&mut rpc, &mint_a_pda, "MintA").await; shared::assert_onchain_closed(&mut rpc, &mint_b_pda, "MintB").await; @@ -251,6 +253,16 @@ async fn test_create_all_derive() { }; let zc_spec = PdaSpec::new(zc_interface, zc_variant, program_id); + // ATA Mint (pre-existing light mint that also gets compressed) + let ata_mint_iface = rpc + .get_mint_interface(&ata_mint, None) + .await + .expect("failed to get ATA mint interface") + .value + .expect("ATA mint interface should exist"); + assert!(ata_mint_iface.is_cold(), "ATA mint should be cold"); + let ata_mint_ai = AccountInterface::from(ata_mint_iface); + // ATA let ata_interface = rpc .get_associated_token_account_interface(&ata_owner, &ata_mint, None) @@ -260,6 +272,16 @@ async fn test_create_all_derive() { .expect("ATA interface should exist"); assert!(ata_interface.is_cold(), "ATA should be cold"); + // Vault Mint (pre-existing light mint that also gets compressed) + let vault_mint_iface = rpc + .get_mint_interface(&vault_mint, None) + .await + .expect("failed to get vault mint interface") + .value + .expect("vault mint interface should exist"); + assert!(vault_mint_iface.is_cold(), "Vault mint should be cold"); + let vault_mint_ai = AccountInterface::from(vault_mint_iface); + // Mint A let mint_a_iface = rpc .get_mint_interface(&mint_a_pda, None) @@ -306,10 +328,13 @@ async fn test_create_all_derive() { }; let vault_spec = PdaSpec::new(vault_interface, vault_variant, program_id); + // Mints must come before ATA and vault since they depend on mints being decompressed let specs: Vec> = vec![ AccountSpec::Pda(record_spec), AccountSpec::Pda(zc_spec), + AccountSpec::Mint(ata_mint_ai), AccountSpec::Ata(Box::new(ata_interface)), + AccountSpec::Mint(vault_mint_ai), AccountSpec::Pda(vault_spec), AccountSpec::Mint(mint_a_ai), AccountSpec::Mint(mint_b_ai), @@ -326,7 +351,9 @@ async fn test_create_all_derive() { // PHASE 4: Assert state preserved after decompression shared::assert_onchain_exists(&mut rpc, &record_pda, "MinimalRecord").await; shared::assert_onchain_exists(&mut rpc, &zc_record_pda, "ZeroCopyRecord").await; + shared::assert_onchain_exists(&mut rpc, &ata_mint, "AtaMint").await; shared::assert_onchain_exists(&mut rpc, &ata, "ATA").await; + shared::assert_onchain_exists(&mut rpc, &vault_mint, "VaultMint").await; shared::assert_onchain_exists(&mut rpc, &vault, "Vault").await; shared::assert_onchain_exists(&mut rpc, &mint_a_pda, "MintA").await; shared::assert_onchain_exists(&mut rpc, &mint_b_pda, "MintB").await; diff --git a/sdk-tests/anchor-semi-manual-test/tests/test_create_ata.rs b/sdk-tests/anchor-semi-manual-test/tests/test_create_ata.rs index 5953b31e71..c58819dff9 100644 --- a/sdk-tests/anchor-semi-manual-test/tests/test_create_ata.rs +++ b/sdk-tests/anchor-semi-manual-test/tests/test_create_ata.rs @@ -88,10 +88,22 @@ async fn test_create_ata_derive() { // PHASE 2: Warp to trigger auto-compression rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &mint, "Mint").await; shared::assert_onchain_closed(&mut rpc, &ata, "ATA").await; // PHASE 3: Decompress via create_load_instructions use anchor_semi_manual_test::LightAccountVariant; + use light_client::interface::AccountInterface; + + // Mint must be decompressed first since ATA depends on it + let mint_iface = rpc + .get_mint_interface(&mint, None) + .await + .expect("failed to get mint interface") + .value + .expect("mint interface should exist"); + assert!(mint_iface.is_cold(), "Mint should be cold"); + let mint_ai = AccountInterface::from(mint_iface); let ata_interface = rpc .get_associated_token_account_interface(&ata_owner, &mint, None) @@ -101,8 +113,11 @@ async fn test_create_ata_derive() { .expect("ATA interface should exist"); assert!(ata_interface.is_cold(), "ATA should be cold"); - let specs: Vec> = - vec![AccountSpec::Ata(Box::new(ata_interface))]; + // Mint must come before ATA since ATA depends on mint being decompressed + let specs: Vec> = vec![ + AccountSpec::Mint(mint_ai), + AccountSpec::Ata(Box::new(ata_interface)), + ]; let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) .await @@ -113,6 +128,7 @@ async fn test_create_ata_derive() { .expect("Decompression should succeed"); // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &mint, "Mint").await; shared::assert_onchain_exists(&mut rpc, &ata, "ATA").await; let actual: Token = shared::parse_token(&rpc.get_account(ata).await.unwrap().unwrap().data); diff --git a/sdk-tests/anchor-semi-manual-test/tests/test_create_token_vault.rs b/sdk-tests/anchor-semi-manual-test/tests/test_create_token_vault.rs index b642ca7357..7f0d7ecd04 100644 --- a/sdk-tests/anchor-semi-manual-test/tests/test_create_token_vault.rs +++ b/sdk-tests/anchor-semi-manual-test/tests/test_create_token_vault.rs @@ -98,9 +98,19 @@ async fn test_create_token_vault_derive() { // PHASE 2: Warp to trigger auto-compression rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &mint, "Mint").await; shared::assert_onchain_closed(&mut rpc, &vault, "Vault").await; - // PHASE 3: Decompress vault + // PHASE 3: Decompress vault (mint must be decompressed first since vault depends on it) + let mint_iface = rpc + .get_mint_interface(&mint, None) + .await + .expect("failed to get mint interface") + .value + .expect("mint interface should exist"); + assert!(mint_iface.is_cold(), "Mint should be cold"); + let mint_ai = AccountInterface::from(mint_iface); + let vault_iface = rpc .get_token_account_interface(&vault, None) .await @@ -126,7 +136,9 @@ async fn test_create_token_vault_derive() { }; let vault_spec = PdaSpec::new(vault_interface, vault_variant, program_id); - let specs: Vec> = vec![AccountSpec::Pda(vault_spec)]; + // Mint must come before vault since vault depends on mint being decompressed + let specs: Vec> = + vec![AccountSpec::Mint(mint_ai), AccountSpec::Pda(vault_spec)]; let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) .await @@ -137,6 +149,7 @@ async fn test_create_token_vault_derive() { .expect("Vault decompression should succeed"); // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &mint, "Mint").await; shared::assert_onchain_exists(&mut rpc, &vault, "Vault").await; let vault_account = rpc diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs index c30ab6369a..050531f970 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs @@ -17,8 +17,8 @@ use csdk_anchor_full_derived_test::amm_test::{ // SDK for AmmSdk-based approach use csdk_anchor_full_derived_test_sdk::{AmmInstruction, AmmSdk}; use light_client::interface::{ - create_load_instructions, get_create_accounts_proof, CreateAccountsProofInput, - InitializeRentFreeConfig, LightProgramInterface, + create_load_instructions, get_create_accounts_proof, AccountInterface, AccountSpec, + CreateAccountsProofInput, InitializeRentFreeConfig, LightProgramInterface, }; use light_compressible::rent::SLOTS_PER_EPOCH; use light_program_test::{ @@ -660,9 +660,30 @@ async fn test_amm_full_lifecycle() { .value .expect("creator_lp_token should exist"); - // add ata - use light_client::interface::AccountSpec; + // Token vaults reference token_0_mint and token_1_mint which may be compressed + // These must be decompressed before the vaults can be decompressed + let mint_0_account_iface = AccountInterface::from( + ctx.rpc + .get_mint_interface(&ctx.token_0_mint, None) + .await + .expect("failed to get token_0_mint") + .value + .expect("token_0_mint should exist"), + ); + + let mint_1_account_iface = AccountInterface::from( + ctx.rpc + .get_mint_interface(&ctx.token_1_mint, None) + .await + .expect("failed to get token_1_mint") + .value + .expect("token_1_mint should exist"), + ); + + // Add mints first (required for vault decompression), then ATA let mut all_specs = specs; + all_specs.push(AccountSpec::Mint(mint_0_account_iface)); + all_specs.push(AccountSpec::Mint(mint_1_account_iface)); all_specs.push(AccountSpec::Ata(Box::new(creator_lp_interface))); let decompress_ixs = diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/d10_token_accounts_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/d10_token_accounts_test.rs index fd04ba17db..32b1e218e5 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/d10_token_accounts_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/d10_token_accounts_test.rs @@ -10,7 +10,9 @@ use csdk_anchor_full_derived_test::d10_token_accounts::{ D10SingleAtaMarkonlyParams, D10SingleAtaParams, D10SingleVaultParams, D10_SINGLE_VAULT_AUTH_SEED, D10_SINGLE_VAULT_SEED, }; -use light_client::interface::{get_create_accounts_proof, InitializeRentFreeConfig}; +use light_client::interface::{ + get_create_accounts_proof, AccountInterface, InitializeRentFreeConfig, +}; use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest}, ProgramTestConfig, Rpc, @@ -540,9 +542,21 @@ async fn test_d10_single_ata_markonly_lifecycle() { "ATA should be cold after compression" ); - // Build AccountSpec for ATA decompression - let specs: Vec> = - vec![AccountSpec::Ata(Box::new(ata_interface))]; + // Mint may also be compressed after time warp - get its interface + let mint_interface = AccountInterface::from( + ctx.rpc + .get_mint_interface(&mint, None) + .await + .expect("failed to get mint") + .value + .expect("mint should exist"), + ); + + // Build AccountSpec - mint first (required for ATA decompression), then ATA + let specs: Vec> = vec![ + AccountSpec::Mint(mint_interface), + AccountSpec::Ata(Box::new(ata_interface)), + ]; // Create decompression instructions let decompress_instructions = diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs index 5cb8b82b1c..9b40b900e5 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs @@ -270,9 +270,22 @@ impl TestContext { cold: Some(vault_compressed.account.clone()), }; - // Create PdaSpec and decompress + // Get mint interface - must be decompressed before token vault + let mint_interface = AccountInterface::from( + self.rpc + .get_mint_interface(&expected_mint, None) + .await + .expect("failed to get mint") + .value + .expect("mint should exist"), + ); + + // Create PdaSpec and decompress - mint first, then vault let vault_spec = PdaSpec::new(vault_interface_for_pda, vault_variant, self.program_id); - let specs: Vec> = vec![AccountSpec::Pda(vault_spec)]; + let specs: Vec> = vec![ + AccountSpec::Mint(mint_interface), + AccountSpec::Pda(vault_spec), + ]; let decompress_instructions = create_load_instructions(&specs, self.payer.pubkey(), self.config_pda, &self.rpc) diff --git a/sdk-tests/pinocchio-light-program-test/tests/stress_test.rs b/sdk-tests/pinocchio-light-program-test/tests/stress_test.rs index 3779e20a5a..c2bb6a4938 100644 --- a/sdk-tests/pinocchio-light-program-test/tests/stress_test.rs +++ b/sdk-tests/pinocchio-light-program-test/tests/stress_test.rs @@ -314,12 +314,13 @@ async fn decompress_all(ctx: &mut StressTestContext, pdas: &TestPdas, cached: &C assert!(mint_iface.is_cold(), "Mint should be cold"); let mint_ai = AccountInterface::from(mint_iface); + // Mint must come before ATA and vault since they depend on mint being decompressed let specs: Vec> = vec![ AccountSpec::Pda(record_spec), AccountSpec::Pda(zc_spec), + AccountSpec::Mint(mint_ai), AccountSpec::Ata(Box::new(ata_interface)), AccountSpec::Pda(vault_spec), - AccountSpec::Mint(mint_ai), ]; let decompress_ixs = diff --git a/sdk-tests/pinocchio-light-program-test/tests/test_create_all.rs b/sdk-tests/pinocchio-light-program-test/tests/test_create_all.rs index c0b9742f13..9c8dd630f8 100644 --- a/sdk-tests/pinocchio-light-program-test/tests/test_create_all.rs +++ b/sdk-tests/pinocchio-light-program-test/tests/test_create_all.rs @@ -291,12 +291,13 @@ async fn test_create_all_derive() { assert!(mint_iface.is_cold(), "Mint should be cold"); let mint_ai = AccountInterface::from(mint_iface); + // Mint must come before ATA and vault since they depend on mint being decompressed let specs: Vec> = vec![ AccountSpec::Pda(record_spec), AccountSpec::Pda(zc_spec), + AccountSpec::Mint(mint_ai), AccountSpec::Ata(Box::new(ata_interface)), AccountSpec::Pda(vault_spec), - AccountSpec::Mint(mint_ai), ]; let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) diff --git a/sdk-tests/pinocchio-light-program-test/tests/test_create_ata.rs b/sdk-tests/pinocchio-light-program-test/tests/test_create_ata.rs index 229bde9dac..a646c05549 100644 --- a/sdk-tests/pinocchio-light-program-test/tests/test_create_ata.rs +++ b/sdk-tests/pinocchio-light-program-test/tests/test_create_ata.rs @@ -81,9 +81,22 @@ async fn test_create_ata_derive() { // PHASE 2: Warp to trigger auto-compression rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &mint, "Mint").await; shared::assert_onchain_closed(&mut rpc, &ata, "ATA").await; // PHASE 3: Decompress via create_load_instructions + use light_client::interface::AccountInterface; + + // Mint must be decompressed first since ATA depends on it + let mint_iface = rpc + .get_mint_interface(&mint, None) + .await + .expect("failed to get mint interface") + .value + .expect("mint interface should exist"); + assert!(mint_iface.is_cold(), "Mint should be cold"); + let mint_ai = AccountInterface::from(mint_iface); + let ata_interface = rpc .get_associated_token_account_interface(&ata_owner, &mint, None) .await @@ -92,8 +105,11 @@ async fn test_create_ata_derive() { .expect("ATA interface should exist"); assert!(ata_interface.is_cold(), "ATA should be cold"); - let specs: Vec> = - vec![AccountSpec::Ata(Box::new(ata_interface))]; + // Mint must come before ATA since ATA depends on mint being decompressed + let specs: Vec> = vec![ + AccountSpec::Mint(mint_ai), + AccountSpec::Ata(Box::new(ata_interface)), + ]; let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) .await @@ -104,6 +120,7 @@ async fn test_create_ata_derive() { .expect("Decompression should succeed"); // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &mint, "Mint").await; shared::assert_onchain_exists(&mut rpc, &ata, "ATA").await; let actual: Token = shared::parse_token(&rpc.get_account(ata).await.unwrap().unwrap().data); diff --git a/sdk-tests/pinocchio-light-program-test/tests/test_create_token_vault.rs b/sdk-tests/pinocchio-light-program-test/tests/test_create_token_vault.rs index 3e83ba60fc..c10a0c69d1 100644 --- a/sdk-tests/pinocchio-light-program-test/tests/test_create_token_vault.rs +++ b/sdk-tests/pinocchio-light-program-test/tests/test_create_token_vault.rs @@ -87,9 +87,19 @@ async fn test_create_token_vault_derive() { // PHASE 2: Warp to trigger auto-compression rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &mint, "Mint").await; shared::assert_onchain_closed(&mut rpc, &vault, "Vault").await; - // PHASE 3: Decompress vault + // PHASE 3: Decompress vault (mint must be decompressed first since vault depends on it) + let mint_iface = rpc + .get_mint_interface(&mint, None) + .await + .expect("failed to get mint interface") + .value + .expect("mint interface should exist"); + assert!(mint_iface.is_cold(), "Mint should be cold"); + let mint_ai = AccountInterface::from(mint_iface); + let vault_iface = rpc .get_token_account_interface(&vault, None) .await @@ -118,7 +128,9 @@ async fn test_create_token_vault_derive() { }; let vault_spec = PdaSpec::new(vault_interface, vault_variant, program_id); - let specs: Vec> = vec![AccountSpec::Pda(vault_spec)]; + // Mint must come before vault since vault depends on mint being decompressed + let specs: Vec> = + vec![AccountSpec::Mint(mint_ai), AccountSpec::Pda(vault_spec)]; let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) .await @@ -129,6 +141,7 @@ async fn test_create_token_vault_derive() { .expect("Vault decompression should succeed"); // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &mint, "Mint").await; shared::assert_onchain_exists(&mut rpc, &vault, "Vault").await; let vault_account = rpc diff --git a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs index 605e9c69d9..30851e769d 100644 --- a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs +++ b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint.rs @@ -1,21 +1,25 @@ // cMint to cToken scenario test - Direct SDK calls without wrapper program // // This test demonstrates the complete flow: -// 1. Create cMint (compressed mint) +// 1. Create cMint (compressed mint with auto-decompress) // 2. Create 2 cToken ATAs for different owners // 3. Mint cTokens to both accounts // 4. Transfer cTokens from account 1 to account 2 -// 5. Advance epochs to trigger compression +// 5. Advance epochs to trigger compression (both cToken ATAs and Mint get compressed) // 6. Verify cToken account is compressed and closed -// 7. Recreate cToken ATA -// 8. Decompress compressed tokens back to cToken account -// 9. Verify cToken account has tokens again +// 7. Decompress the light mint (compressed when epochs advanced) +// 8. Recreate cToken ATA +// 9. Decompress compressed tokens back to cToken account +// 10. Verify cToken account has tokens again mod shared; use borsh::BorshDeserialize; use light_client::{indexer::Indexer, rpc::Rpc}; use light_program_test::{program_test::TestRpc, LightProgramTest, ProgramTestConfig}; +use light_test_utils::actions::legacy::instructions::mint_action::{ + create_mint_action_instruction, MintActionParams, MintActionType, +}; use light_token::instruction::{CreateAssociatedTokenAccount, Decompress, Token, Transfer}; use solana_sdk::{signature::Keypair, signer::Signer}; @@ -46,7 +50,7 @@ async fn test_mint_to_ctoken_scenario() { let mint_amount2 = 5_000u64; let transfer_amount = 3_000u64; - let (mint, _compression_address, ata_pubkeys, _mint_seed) = shared::setup_create_mint( + let (mint, compression_address, ata_pubkeys, mint_seed) = shared::setup_create_mint( &mut rpc, &payer, payer.pubkey(), // mint_authority @@ -185,7 +189,31 @@ async fn test_mint_to_ctoken_scenario() { compressed_account.token.amount ); - // 9. Recreate cToken ATA for decompression (idempotent) + // 9. Decompress the light mint (which was compressed when epochs advanced) + println!("\nDecompressing light mint..."); + let decompress_mint_ix = create_mint_action_instruction( + &mut rpc, + MintActionParams { + compressed_mint_address: compression_address, + mint_seed: mint_seed.pubkey(), + authority: payer.pubkey(), // mint_authority from setup_create_mint + payer: payer.pubkey(), + actions: vec![MintActionType::DecompressMint { + rent_payment: 16, + write_top_up: 766, + }], + new_mint: None, + }, + ) + .await + .unwrap(); + + rpc.create_and_send_transaction(&[decompress_mint_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + println!(" - Light mint decompressed"); + + // 10. Recreate cToken ATA for decompression (idempotent) println!("\nRecreating cToken ATA for decompression..."); let create_ata_instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), owner2.pubkey(), mint) @@ -207,7 +235,7 @@ async fn test_mint_to_ctoken_scenario() { let deserialized_ata = Token::try_from_slice(ctoken_account_data.data.as_slice()).unwrap(); println!("deserialized ata {:?}", deserialized_ata); - // 10. Get validity proof for the compressed account + // 11. Get validity proof for the compressed account let compressed_hashes: Vec<_> = compressed_accounts .iter() .map(|acc| acc.account.hash) @@ -231,7 +259,7 @@ async fn test_mint_to_ctoken_scenario() { // Get tree info from validity proof result let account_proof = &rpc_result.accounts[0]; - // 11. Decompress compressed tokens to cToken account + // 12. Decompress compressed tokens to cToken account // For ATA decompress, the wallet owner (owner2) must sign println!("Decompressing tokens to cToken account..."); println!("discriminator {:?}", discriminator); @@ -259,7 +287,7 @@ async fn test_mint_to_ctoken_scenario() { .await .unwrap(); - // 12. Verify compressed accounts are consumed + // 13. Verify compressed accounts are consumed let remaining_compressed = rpc .get_compressed_token_accounts_by_owner(&ctoken_ata2, None, None) .await @@ -274,7 +302,7 @@ async fn test_mint_to_ctoken_scenario() { ); println!(" - Compressed accounts consumed"); - // 13. Verify cToken account has tokens again + // 14. Verify cToken account has tokens again let ctoken_account_data = rpc.get_account(ctoken_ata2).await.unwrap().unwrap(); let ctoken_account = Token::deserialize(&mut &ctoken_account_data.data[..]).unwrap(); let decompressed_balance = ctoken_account.amount; diff --git a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs index c653cd8269..5651cf95a2 100644 --- a/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs +++ b/sdk-tests/sdk-light-token-test/tests/scenario_light_mint_compression_only.rs @@ -1,21 +1,25 @@ // cMint to cToken scenario test with compression_only: true // // This test demonstrates the complete flow with compression_only flag enabled: -// 1. Create cMint (compressed mint) +// 1. Create cMint (compressed mint with auto-decompress) // 2. Create 2 cToken ATAs for different owners with compression_only: true // 3. Mint cTokens to both accounts // 4. Transfer cTokens from account 1 to account 2 -// 5. Advance epochs to trigger compression +// 5. Advance epochs to trigger compression (both cToken ATAs and Mint get compressed) // 6. Verify cToken account is compressed and closed (with TLV data) -// 7. Recreate cToken ATA -// 8. Decompress compressed tokens back to cToken account -// 9. Verify cToken account has tokens again +// 7. Decompress the light mint (compressed when epochs advanced) +// 8. Recreate cToken ATA +// 9. Decompress compressed tokens back to cToken account +// 10. Verify cToken account has tokens again mod shared; use borsh::BorshDeserialize; use light_client::{indexer::Indexer, rpc::Rpc}; use light_program_test::{program_test::TestRpc, LightProgramTest, ProgramTestConfig}; +use light_test_utils::actions::legacy::instructions::mint_action::{ + create_mint_action_instruction, MintActionParams, MintActionType, +}; use light_token::instruction::{ CompressibleParams, CreateAssociatedTokenAccount, Decompress, Token, Transfer, }; @@ -49,7 +53,7 @@ async fn test_mint_to_ctoken_scenario_compression_only() { let transfer_amount = 3_000u64; // Use compression_only: true for this test - let (mint, _compression_address, ata_pubkeys) = + let (mint, compression_address, ata_pubkeys, mint_seed) = shared::setup_create_mint_with_compression_only( &mut rpc, &payer, @@ -189,7 +193,31 @@ async fn test_mint_to_ctoken_scenario_compression_only() { compressed_account.token.amount ); - // 9. Recreate cToken ATA for decompression (idempotent) with compression_only: true + // 9. Decompress the light mint (which was compressed when epochs advanced) + println!("\nDecompressing light mint..."); + let decompress_mint_ix = create_mint_action_instruction( + &mut rpc, + MintActionParams { + compressed_mint_address: compression_address, + mint_seed: mint_seed.pubkey(), + authority: payer.pubkey(), // mint_authority from setup_create_mint_with_compression_only + payer: payer.pubkey(), + actions: vec![MintActionType::DecompressMint { + rent_payment: 16, + write_top_up: 766, + }], + new_mint: None, + }, + ) + .await + .unwrap(); + + rpc.create_and_send_transaction(&[decompress_mint_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + println!(" - Light mint decompressed"); + + // 10. Recreate cToken ATA for decompression (idempotent) with compression_only: true println!("\nRecreating cToken ATA for decompression..."); let compressible_params = CompressibleParams { compression_only: true, @@ -216,7 +244,7 @@ async fn test_mint_to_ctoken_scenario_compression_only() { let deserialized_ata = Token::try_from_slice(ctoken_account_data.data.as_slice()).unwrap(); println!("deserialized ata {:?}", deserialized_ata); - // 10. Get validity proof for the compressed account + // 11. Get validity proof for the compressed account let compressed_hashes: Vec<_> = compressed_accounts .iter() .map(|acc| acc.account.hash) @@ -240,7 +268,7 @@ async fn test_mint_to_ctoken_scenario_compression_only() { // Get tree info from validity proof result let account_proof = &rpc_result.accounts[0]; - // 11. Decompress compressed tokens to cToken account + // 12. Decompress compressed tokens to cToken account println!("Decompressing tokens to cToken account..."); println!("discriminator {:?}", discriminator); println!("token_data {:?}", token_data); @@ -267,7 +295,7 @@ async fn test_mint_to_ctoken_scenario_compression_only() { .await .unwrap(); - // 12. Verify compressed accounts are consumed + // 13. Verify compressed accounts are consumed let remaining_compressed = rpc .get_compressed_token_accounts_by_owner(&ctoken_ata2, None, None) .await @@ -282,7 +310,7 @@ async fn test_mint_to_ctoken_scenario_compression_only() { ); println!(" - Compressed accounts consumed"); - // 13. Verify cToken account has tokens again + // 14. Verify cToken account has tokens again let ctoken_account_data = rpc.get_account(ctoken_ata2).await.unwrap().unwrap(); let ctoken_account = Token::deserialize(&mut &ctoken_account_data.data[..]).unwrap(); let decompressed_balance = ctoken_account.amount; diff --git a/sdk-tests/sdk-light-token-test/tests/shared.rs b/sdk-tests/sdk-light-token-test/tests/shared.rs index d3046d227c..79f591ffa3 100644 --- a/sdk-tests/sdk-light-token-test/tests/shared.rs +++ b/sdk-tests/sdk-light-token-test/tests/shared.rs @@ -268,7 +268,7 @@ pub async fn setup_create_mint_with_compression_only( decimals: u8, recipients: Vec<(u64, Pubkey)>, compression_only: bool, -) -> (Pubkey, [u8; 32], Vec) { +) -> (Pubkey, [u8; 32], Vec, Keypair) { use light_token::instruction::{ CompressibleParams, CreateAssociatedTokenAccount, CreateMint, CreateMintParams, MintTo, }; @@ -343,7 +343,7 @@ pub async fn setup_create_mint_with_compression_only( // If no recipients, return early if recipients.is_empty() { - return (mint, compression_address, vec![]); + return (mint, compression_address, vec![], mint_seed); } // Create ATAs for each recipient with custom compression_only setting @@ -395,7 +395,7 @@ pub async fn setup_create_mint_with_compression_only( .unwrap(); } - (mint, compression_address, ata_pubkeys) + (mint, compression_address, ata_pubkeys, mint_seed) } /// Creates a compressed-only mint (no decompression) using light-token-client. diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs index 77c7e1dd43..0d803802b2 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs @@ -4,7 +4,7 @@ use light_compressed_token_sdk::compressed_token::{ }; use light_sdk_types::cpi_accounts::v2::CpiAccounts; use light_token_interface::instructions::mint_action::{ - MintActionCompressedInstructionData, MintToAction, MintToCompressedAction, UpdateAuthority, + MintActionCompressedInstructionData, MintToCompressedAction, UpdateAuthority, }; use super::{processor::ChainedCtokenInstructionData, PdaCToken}; @@ -17,6 +17,10 @@ pub fn process_mint_action<'a, 'info>( // Build instruction data using builder pattern // ValidityProof is a wrapper around Option let compressed_proof = input.pda_creation.proof.0.unwrap(); + // NOTE: .with_mint_to() removed because MintToCToken requires an ATA, but ATAs + // require the mint to exist first. Since the mint is created in this same instruction, + // we cannot have an ATA ready. MintToCToken should be tested separately after mint + // decompression. let instruction_data = MintActionCompressedInstructionData::new_mint( input.compressed_mint_with_context.root_index, compressed_proof, @@ -26,10 +30,6 @@ pub fn process_mint_action<'a, 'info>( token_account_version: 2, recipients: input.token_recipients.clone(), }) - .with_mint_to(MintToAction { - account_index: 0, // Index in remaining accounts - amount: input.token_recipients[0].amount, - }) .with_update_mint_authority(UpdateAuthority { new_authority: input .final_mint_authority @@ -50,7 +50,6 @@ pub fn process_mint_action<'a, 'info>( // Build account structure for CPI - manually construct from CpiAccounts let tree_accounts = cpi_accounts.tree_accounts().unwrap(); - let ctoken_accounts_vec = vec![ctx.accounts.token_account.to_account_info()]; let mint_action_accounts = MintActionCpiAccounts { compressed_token_program: ctx.accounts.light_token_program.as_ref(), light_system_program: cpi_accounts.system_program().unwrap(), @@ -66,8 +65,8 @@ pub fn process_mint_action<'a, 'info>( out_output_queue: &tree_accounts[1], // output queue in_merkle_tree: &tree_accounts[0], // address tree in_output_queue: None, // Not needed for create - tokens_out_queue: Some(&tree_accounts[0]), // Same as output queue for mint_to - ctoken_accounts: &ctoken_accounts_vec, // For MintToCToken + tokens_out_queue: Some(&tree_accounts[0]), // Required even without MintToCToken + ctoken_accounts: &[], // Not using MintToCToken }; // Build instruction using trait method @@ -80,7 +79,6 @@ pub fn process_mint_action<'a, 'info>( account_infos.push(ctx.accounts.mint_authority.to_account_info()); account_infos.push(ctx.accounts.mint_seed.to_account_info()); account_infos.push(ctx.accounts.payer.to_account_info()); - account_infos.push(ctx.accounts.token_account.to_account_info()); msg!("mint_action_instruction {:?}", mint_action_instruction); msg!( "account infos pubkeys {:?}", diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs index 5c3ef6be29..d5e9efc15e 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs @@ -13,9 +13,6 @@ pub struct PdaCToken<'info> { pub mint_authority: Signer<'info>, pub mint_seed: Signer<'info>, /// CHECK: - #[account(mut)] - pub token_account: UncheckedAccount<'info>, - /// CHECK: pub light_token_program: UncheckedAccount<'info>, /// CHECK: pub light_token_cpi_authority: UncheckedAccount<'info>, diff --git a/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs b/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs index 9cc64620c9..5f096af560 100644 --- a/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs +++ b/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs @@ -1,6 +1,6 @@ //#![cfg(feature = "test-sbf")] -use anchor_lang::{AnchorDeserialize, InstructionData}; +use anchor_lang::InstructionData; /// Test input range for multi-input tests const TEST_INPUT_RANGE: [usize; 4] = [1, 2, 3, 4]; @@ -13,8 +13,7 @@ use light_test_utils::{ actions::{legacy::instructions::mint_action::NewMint, mint_action_comprehensive}, airdrop_lamports, }; -use light_token_interface::instructions::mint_action::{MintWithContext, Recipient}; -use sdk_token_test::mint_compressed_tokens_cpi_write::MintCompressedTokensCpiWriteParams; +use light_token_interface::instructions::mint_action::Recipient; use solana_sdk::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, @@ -27,6 +26,7 @@ use solana_sdk::{ struct TestContext { payer: Keypair, owner: Keypair, + #[allow(dead_code)] mint_seed: Keypair, mint_pubkey: Pubkey, destination_accounts: Vec, @@ -56,10 +56,49 @@ async fn setup_decompress_full_test(num_inputs: usize) -> (LightProgramTest, Tes .await .unwrap(); + use light_test_utils::actions::legacy::instructions::mint_action::DecompressMintParams; use light_token::instruction::{ derive_token_ata, CompressibleParams, CreateAssociatedTokenAccount, }; + let total_compressed_amount = 1000; + let compressed_amount_per_account = total_compressed_amount / num_inputs as u64; + + let compressed_recipients: Vec = (0..num_inputs) + .map(|_| Recipient::new(owner.pubkey(), compressed_amount_per_account)) + .collect(); + + println!( + "Minting {} tokens to {} compressed accounts ({} per account) for owner", + total_compressed_amount, num_inputs, compressed_amount_per_account + ); + + // First create AND decompress the mint (CToken ATA creation requires mint to exist on-chain) + // Also mint compressed tokens in the same call + mint_action_comprehensive( + &mut rpc, + &mint_seed, + &payer, + &payer, + Some(DecompressMintParams::default()), // decompress mint so it exists on-chain + false, // compress_and_close_mint + compressed_recipients, + Vec::new(), + None, + None, + Some(NewMint { + decimals, + mint_authority, + supply: 0, + freeze_authority: None, + metadata: None, + version: 3, + }), + ) + .await + .unwrap(); + + // Now create destination ATAs - mint exists on-chain let mut destination_accounts = Vec::with_capacity(num_inputs); for i in 0..num_inputs { @@ -101,41 +140,6 @@ async fn setup_decompress_full_test(num_inputs: usize) -> (LightProgramTest, Tes destination_accounts.push(destination_account); } - let total_compressed_amount = 1000; - let compressed_amount_per_account = total_compressed_amount / num_inputs as u64; - - let compressed_recipients: Vec = (0..num_inputs) - .map(|_| Recipient::new(owner.pubkey(), compressed_amount_per_account)) - .collect(); - - println!( - "Minting {} tokens to {} compressed accounts ({} per account) for owner", - total_compressed_amount, num_inputs, compressed_amount_per_account - ); - - mint_action_comprehensive( - &mut rpc, - &mint_seed, - &payer, - &payer, - None, // decompress_mint - false, // compress_and_close_mint - compressed_recipients, - Vec::new(), - None, - None, - Some(NewMint { - decimals, - mint_authority, - supply: 0, - freeze_authority: None, - metadata: None, - version: 3, - }), - ) - .await - .unwrap(); - ( rpc, TestContext { @@ -303,8 +307,16 @@ async fn test_decompress_full_cpi() { } } -/// Test decompress_full with CPI context for optimized multi-program transactions -/// This test uses CPI context to cache signer checks for potential cross-program operations +/// Test decompress_full with the CPI context instruction variant +/// +/// NOTE: After the mint validation change, this test no longer uses CPI context because: +/// 1. CToken ATAs require an on-chain (decompressed) mint +/// 2. MintWithContext requires a compressed mint +/// 3. The program ties CPI context to minting (with_cpi_context = params.is_some()) +/// +/// Since these constraints are mutually exclusive, we can only test the instruction +/// variant without actually using CPI context. The core DecompressFull functionality +/// is tested in test_decompress_full_cpi. #[tokio::test] async fn test_decompress_full_cpi_with_context() { for num_inputs in TEST_INPUT_RANGE { @@ -346,38 +358,6 @@ async fn test_decompress_full_cpi_with_context() { } let mut remaining_accounts = PackedAccounts::default(); - // let output_tree_info = rpc.get_random_state_tree_info().unwrap(); - - let mint_recipients = vec![Recipient::new(ctx.owner.pubkey(), 500)]; - - let address_tree_info = rpc.get_address_tree_v2(); - let compressed_mint_address = - light_compressed_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address( - &ctx.mint_seed.pubkey(), - &address_tree_info.tree, - ); - - let compressed_mint_account = rpc - .get_compressed_account(compressed_mint_address, None) - .await - .unwrap() - .value - .ok_or("Compressed mint account not found") - .unwrap(); - println!( - "compressed_mint_account - .tree_info {:?}", - compressed_mint_account.tree_info - ); - let cpi_context_pubkey = compressed_mint_account - .tree_info - .cpi_context - .expect("CPI context required for this test"); - - let config = DecompressFullAccounts::new(Some(cpi_context_pubkey)); - remaining_accounts - .add_custom_system_accounts(config) - .unwrap(); let compressed_hashes: Vec<_> = initial_compressed_accounts .iter() @@ -389,37 +369,12 @@ async fn test_decompress_full_cpi_with_context() { .unwrap() .value; - use light_token_interface::state::Mint; - let compressed_mint = - Mint::deserialize(&mut compressed_mint_account.data.unwrap().data.as_slice()).unwrap(); - - let compressed_mint_with_context = MintWithContext { - prove_by_index: true, - leaf_index: compressed_mint_account.leaf_index, - root_index: 0, - address: compressed_mint_address, - mint: Some(compressed_mint.try_into().unwrap()), - }; + // Add tree accounts first, then custom system accounts (no CPI context since params is None) let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts); - let mint_params = MintCompressedTokensCpiWriteParams { - compressed_mint_with_context, - recipients: mint_recipients, - cpi_context: light_token_interface::instructions::mint_action::CpiContext { - set_context: false, - first_set_context: true, // First operation sets the context - in_tree_index: remaining_accounts - .insert_or_get(compressed_mint_account.tree_info.tree), - in_queue_index: remaining_accounts - .insert_or_get(compressed_mint_account.tree_info.queue), - out_queue_index: remaining_accounts - .insert_or_get(compressed_mint_account.tree_info.queue), - token_out_queue_index: remaining_accounts - .insert_or_get(compressed_mint_account.tree_info.queue), - assigned_account_index: 0, - ..Default::default() - }, - cpi_context_pubkey, - }; + let config = DecompressFullAccounts::new(None); + remaining_accounts + .add_custom_system_accounts(config) + .unwrap(); let token_data: Vec<_> = initial_compressed_accounts .iter() @@ -461,21 +416,12 @@ async fn test_decompress_full_cpi_with_context() { let validity_proof = rpc_result.proof; - let (account_metas, system_accounts_start_offset, _) = - remaining_accounts.to_account_metas(); - - println!("CPI Context test:"); - println!(" CPI context account: {:?}", cpi_context_pubkey); - println!(" Destination accounts: {:?}", ctx.destination_accounts); - println!( - " System accounts start offset: {}", - system_accounts_start_offset - ); + let (account_metas, _, _) = remaining_accounts.to_account_metas(); let instruction_data = sdk_token_test::instruction::DecompressFullCpiWithCpiContext { indices, validity_proof, - params: Some(mint_params), + params: None, }; let instruction = Instruction { @@ -493,6 +439,7 @@ async fn test_decompress_full_cpi_with_context() { rpc.process_transaction(transaction).await.unwrap(); + // All compressed accounts should be consumed (decompressed) let final_compressed_accounts = rpc .get_compressed_token_accounts_by_owner(&ctx.owner.pubkey(), None, None) .await @@ -502,14 +449,9 @@ async fn test_decompress_full_cpi_with_context() { assert_eq!( final_compressed_accounts.len(), - 1, - "Should have 1 compressed account (newly minted 500 tokens)" - ); - assert_eq!( - final_compressed_accounts[0].token.amount, 500, - "Newly minted compressed tokens" + 0, + "All compressed accounts should be consumed" ); - assert_eq!(final_compressed_accounts[0].token.mint, ctx.mint_pubkey); for destination_account in &ctx.destination_accounts { let dest_account_after = rpc @@ -528,13 +470,12 @@ async fn test_decompress_full_cpi_with_context() { } println!( - "✅ DecompressFull CPI with CPI context test passed with {} inputs!", + "DecompressFull CPI (context variant) test passed with {} inputs!", num_inputs ); println!( - " - Original {} tokens decompressed to {} destinations ({} each)", + " - {} tokens decompressed to {} destinations ({} each)", ctx.total_compressed_amount, num_inputs, ctx.compressed_amount_per_account ); - println!(" - Additional 500 tokens minted to compressed state"); } } diff --git a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs index 990743a5a1..fcae1ce1f7 100644 --- a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs +++ b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs @@ -1,7 +1,4 @@ -use anchor_lang::{ - solana_program::program_pack::Pack, AnchorDeserialize, InstructionData, ToAccountMetas, -}; -use anchor_spl::token_interface::spl_token_2022; +use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; use light_client::indexer::Indexer; use light_compressed_account::{address::derive_address, hash_to_bn254_field_size_be}; use light_compressed_token_sdk::compressed_token::create_compressed_mint::{ @@ -9,9 +6,6 @@ use light_compressed_token_sdk::compressed_token::create_compressed_mint::{ }; use light_program_test::{LightProgramTest, ProgramTestConfig, Rpc, RpcError}; use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; -use light_token::instruction::{ - derive_token_ata, CompressibleParams, CreateAssociatedTokenAccount, -}; use light_token_interface::{ instructions::{ extensions::token_metadata::TokenMetadataInstructionData, @@ -67,7 +61,10 @@ async fn test_pda_ctoken() { }; // Create the compressed mint (with chained operations including update mint) - let (compressed_mint_address, token_account, mint) = create_mint( + // NOTE: MintToCToken is not tested here because ATAs require the mint to exist first, + // and the mint is created in this same instruction. MintToCToken should be tested + // separately after mint decompression. + let compressed_mint_address = create_mint( &mut rpc, &mint_seed, decimals, @@ -121,8 +118,8 @@ async fn test_pda_ctoken() { "Mint authority should be revoked (None)" ); assert_eq!( - compressed_mint.base.supply, 2000u64, - "Supply should be 2000 after minting (1000 regular + 1000 from MintToCToken)" + compressed_mint.base.supply, 1000u64, + "Supply should be 1000 after minting to compressed accounts" ); assert_eq!( compressed_mint.base.decimals, decimals, @@ -133,28 +130,9 @@ async fn test_pda_ctoken() { let token_accounts = rpc .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) .await - .unwrap(); - - // 3. Verify decompressed tokens were minted to the token account - let token_account_info = rpc.get_account(token_account).await.unwrap().unwrap(); - let token_account_data = - spl_token_2022::state::Account::unpack(&token_account_info.data[..165]).unwrap(); - - assert_eq!( - token_account_data.amount, 1000u64, - "Token account should have 1000 tokens from MintToCToken action" - ); - assert_eq!( - token_account_data.owner, - mint_authority_keypair.pubkey(), - "Token account should be owned by mint authority" - ); - assert_eq!( - token_account_data.mint, mint, - "Token account should be associated with the SPL mint" - ); - - let token_accounts = token_accounts.value.items; + .unwrap() + .value + .items; println!("✅ Tokens minted:"); println!(" - Token accounts found: {}", token_accounts.len()); @@ -171,11 +149,11 @@ async fn test_pda_ctoken() { "Token amount should be 1000" ); - println!("🎉 All chained CPI operations completed successfully!"); - println!(" 1. ✅ Created compressed mint with mint authority"); - println!(" 2. ✅ Minted 1000 tokens to payer"); - println!(" 3. ✅ Revoked mint authority (set to None)"); - println!(" 4. ✅ Created escrow PDA"); + println!("All chained CPI operations completed successfully!"); + println!(" 1. Created compressed mint with mint authority"); + println!(" 2. Minted 1000 tokens to payer (compressed)"); + println!(" 3. Revoked mint authority (set to None)"); + println!(" 4. Created escrow PDA"); } pub async fn create_mint( @@ -186,7 +164,7 @@ pub async fn create_mint( freeze_authority: Option, metadata: Option, payer: &Keypair, -) -> Result<([u8; 32], Pubkey, Pubkey), RpcError> { +) -> Result<[u8; 32], RpcError> { // Get address tree and output queue from RPC let address_tree_pubkey = rpc.get_address_tree_v2().tree; @@ -199,31 +177,9 @@ pub async fn create_mint( // Find mint bump for the instruction let (mint, mint_bump) = find_mint_address(&mint_seed.pubkey()); - // Create compressed token associated token account for the mint authority - let token_account = derive_token_ata(&mint_authority.pubkey(), &mint); - println!("Created token_account (ATA): {:?}", token_account); - - let compressible_params = CompressibleParams { - compressible_config: rpc - .test_accounts - .funding_pool_config - .compressible_config_pda, - rent_sponsor: rpc.test_accounts.funding_pool_config.rent_sponsor_pda, - pre_pay_num_epochs: 2, - lamports_per_write: Some(1000), - compress_to_account_pubkey: None, - token_account_version: light_token_interface::state::TokenDataVersion::ShaFlat, - compression_only: true, - }; - - let create_ata_instruction = - CreateAssociatedTokenAccount::new(payer.pubkey(), mint_authority.pubkey(), mint) - .with_compressible(compressible_params) - .instruction() - .unwrap(); - rpc.create_and_send_transaction(&[create_ata_instruction], &payer.pubkey(), &[payer]) - .await - .expect("Failed to create associated token account"); + // NOTE: ATA creation removed because ATAs require the mint to exist first, + // and the mint is created in this instruction. MintToCToken should be tested + // separately after mint decompression. let pda_address_seed = hash_to_bn254_field_size_be( [b"escrow", payer.pubkey().to_bytes().as_ref()] @@ -301,7 +257,6 @@ pub async fn create_mint( mint_seed: mint_seed.pubkey(), light_token_program: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), light_token_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), - token_account, }; let pda_new_address_params = light_sdk::address::NewAddressParamsAssignedPacked { @@ -346,6 +301,6 @@ pub async fn create_mint( rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &signers) .await?; - // Return the compressed mint address, token account, and SPL mint - Ok((compressed_mint_address, token_account, mint)) + // Return the compressed mint address + Ok(compressed_mint_address) } diff --git a/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs b/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs index 488a63a35e..991b44d6b4 100644 --- a/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs +++ b/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs @@ -12,7 +12,6 @@ use light_sdk::{ instruction::{PackedAccounts, PackedStateTreeInfo, SystemAccountMetaConfig}, }; use light_test_utils::RpcError; -use light_token::instruction::CreateAssociatedTokenAccount; use light_token_interface::{ instructions::{ mint_action::{MintWithContext, Recipient}, @@ -79,25 +78,47 @@ async fn create_compressed_mints_and_tokens( payer: &Keypair, ) -> (Pubkey, Pubkey, Pubkey, Pubkey) { let decimals = 6u8; - let compress_amount = 1000; // Amount to mint as compressed tokens + let compress_amount = 1000; - // Create 3 compressed mints - let (mint1_pda, mint1_pubkey) = create_compressed_mint_helper(rpc, payer, decimals).await; - let (mint2_pda, mint2_pubkey) = create_compressed_mint_helper(rpc, payer, decimals).await; - let (mint3_pda, mint3_pubkey) = create_compressed_mint_helper(rpc, payer, decimals).await; + // Create 3 compressed mints - keep mint1_seed for decompression + let (mint1_seed, mint1_pda, mint1_pubkey) = + create_compressed_mint_helper(rpc, payer, decimals).await; + let (_, mint2_pda, mint2_pubkey) = create_compressed_mint_helper(rpc, payer, decimals).await; + let (_, mint3_pda, mint3_pubkey) = create_compressed_mint_helper(rpc, payer, decimals).await; println!("Created compressed mint 1: {}", mint1_pubkey); println!("Created compressed mint 2: {}", mint2_pubkey); println!("Created compressed mint 3: {}", mint3_pubkey); - // Mint compressed tokens for all three mints - mint_compressed_tokens(rpc, payer, &mint1_pda, mint1_pubkey, compress_amount).await; + // Mint compressed tokens for mint2 and mint3 (for transfer tests) mint_compressed_tokens(rpc, payer, &mint2_pda, mint2_pubkey, compress_amount).await; mint_compressed_tokens(rpc, payer, &mint3_pda, mint3_pubkey, compress_amount).await; - // Create associated token account for mint1 decompression - let token_account1_pubkey = - light_token::instruction::derive_token_ata(&payer.pubkey(), &mint1_pda); + // Step 1: Decompress mint1 so it exists on-chain (required for CToken ATA creation) + use light_test_utils::actions::{ + legacy::instructions::mint_action::DecompressMintParams, mint_action_comprehensive, + }; + + mint_action_comprehensive( + rpc, + &mint1_seed, + payer, + payer, + Some(DecompressMintParams::default()), + false, + vec![], + vec![], + None, + None, + None, + ) + .await + .unwrap(); + + // Step 2: Create CToken ATA for mint1 (mint now exists on-chain) + use light_token::instruction::{derive_token_ata, CreateAssociatedTokenAccount}; + let token_account1_pubkey = derive_token_ata(&payer.pubkey(), &mint1_pda); + let create_ata_instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), mint1_pda) .instruction() @@ -106,41 +127,26 @@ async fn create_compressed_mints_and_tokens( .await .unwrap(); - // Decompress some compressed tokens for mint1 into the associated token account - let decompress_amount = 500u64; - let compressed_token_accounts = rpc - .indexer() - .unwrap() - .get_compressed_token_accounts_by_owner(&payer.pubkey(), None, None) - .await - .unwrap() - .value - .items; - - let mint1_token_account = compressed_token_accounts - .iter() - .find(|acc| acc.token.mint == mint1_pda) - .expect("Compressed token account for mint1 should exist"); - - let decompress_instruction = - light_test_utils::actions::legacy::instructions::transfer2::create_decompress_instruction( - rpc, - std::slice::from_ref(mint1_token_account), - decompress_amount, - token_account1_pubkey, - payer.pubkey(), - 9, - ) - .await - .unwrap(); - - rpc.create_and_send_transaction(&[decompress_instruction], &payer.pubkey(), &[payer]) - .await - .unwrap(); + // Step 3: Mint tokens to the CToken ATA (via decompressed_recipients) + mint_action_comprehensive( + rpc, + &mint1_seed, + payer, + payer, + None, + false, + vec![], + vec![Recipient::new(payer.pubkey(), 500)], + None, + None, + None, + ) + .await + .unwrap(); println!( - "✅ Minted {} compressed tokens for all three mints and decompressed {} tokens for mint1", - compress_amount, decompress_amount + "Created decompressed mint {} with 500 tokens in ATA {} for compression test", + mint1_pda, token_account1_pubkey ); (mint1_pda, mint2_pda, mint3_pda, token_account1_pubkey) @@ -150,7 +156,7 @@ async fn create_compressed_mint_helper( rpc: &mut LightProgramTest, payer: &Keypair, decimals: u8, -) -> (Pubkey, Pubkey) { +) -> (Keypair, Pubkey, Pubkey) { let mint_authority = payer.pubkey(); let mint_signer = Keypair::new(); let address_tree_pubkey = rpc.get_address_tree_v2().tree; @@ -206,7 +212,7 @@ async fn create_compressed_mint_helper( .await .unwrap(); - (mint_pda, compressed_mint_address.into()) + (mint_signer, mint_pda, compressed_mint_address.into()) } async fn mint_compressed_tokens( diff --git a/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs b/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs index c2d64eeaad..99e99751e7 100644 --- a/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs +++ b/sdk-tests/sdk-token-test/tests/test_compress_full_and_close.rs @@ -175,6 +175,29 @@ async fn test_compress_full_and_close() { println!("✅ Minted {} compressed tokens to recipient", mint_amount); + // Step 3: Decompress the mint so it exists on-chain (required for CToken ATA creation) + use light_test_utils::actions::{ + legacy::instructions::mint_action::DecompressMintParams, mint_action_comprehensive, + }; + + mint_action_comprehensive( + &mut rpc, + &mint_signer, + &mint_authority_keypair, + &payer, + Some(DecompressMintParams::default()), + false, + vec![], + vec![], + None, + None, + None, + ) + .await + .unwrap(); + + println!("✅ Decompressed mint: {}", mint_pda); + // Step 4: Create compressible associated token account for decompression let ctoken_ata_pubkey = derive_token_ata(&recipient, &mint_pda); let compressible_params = CompressibleParams {