diff --git a/Cargo.lock b/Cargo.lock index a6305c984a..8b94c8701a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -5653,7 +5653,7 @@ version = "1.1.0" dependencies = [ "account-compression", "anchor-lang", - "anchor-spl", + "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "borsh 0.10.4", "forester-utils", "light-account-checks", diff --git a/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs b/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs index 3df688aada..bde10a7ec7 100644 --- a/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs +++ b/program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs @@ -185,3 +185,163 @@ async fn test_spl_to_ctoken_transfer() { println!("Successfully completed round-trip transfer: SPL -> CToken -> SPL"); } + +#[tokio::test] +async fn test_ctoken_to_spl_with_compress_and_close() { + use light_compressed_token_sdk::{ + instructions::create_ctoken_to_spl_transfer_and_close_instruction, + token_pool::find_token_pool_pda_with_index, + }; + + let mut rpc = LightProgramTest::new(ProgramTestConfig::new(true, None)) + .await + .unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let sender = Keypair::new(); + airdrop_lamports(&mut rpc, &sender.pubkey(), 1_000_000_000) + .await + .unwrap(); + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create SPL token account and mint tokens + let spl_token_account_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &spl_token_account_keypair, &sender, false) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &spl_token_account_keypair.pubkey(), + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + // Create recipient for compressed tokens + let recipient = Keypair::new(); + airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000) + .await + .unwrap(); + + // Create compressed token ATA for recipient + let instruction = light_compressed_token_sdk::instructions::create_associated_token_account( + payer.pubkey(), + recipient.pubkey(), + mint, + ) + .map_err(|e| RpcError::AssertRpcError(format!("Failed to create ATA instruction: {}", e))) + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + let associated_token_account = derive_ctoken_ata(&recipient.pubkey(), &mint).0; + + // Transfer SPL to CToken + transfer2::spl_to_ctoken_transfer( + &mut rpc, + spl_token_account_keypair.pubkey(), + associated_token_account, + transfer_amount, + &sender, + &payer, + ) + .await + .unwrap(); + + // Verify compressed token balance after initial transfer + { + let ctoken_account_data = rpc + .get_account(associated_token_account) + .await + .unwrap() + .unwrap(); + let ctoken_account = + spl_pod::bytemuck::pod_from_bytes::(&ctoken_account_data.data[..165]) + .map_err(|e| { + RpcError::AssertRpcError(format!("Failed to parse CToken account: {}", e)) + }) + .unwrap(); + assert_eq!( + u64::from(ctoken_account.amount), + transfer_amount, + "Recipient should have {} compressed tokens", + transfer_amount + ); + } + + // Now transfer back using CompressAndClose instead of regular transfer + println!("Testing reverse transfer with CompressAndClose: ctoken to SPL"); + + // Get token pool PDA + let (token_pool_pda, token_pool_pda_bump) = find_token_pool_pda_with_index(&mint, 0); + + // Create instruction using compress_and_close variant + // Note: Using spl_token::ID because create_mint_helper creates Token (not Token-2022) mints + let transfer_ix = create_ctoken_to_spl_transfer_and_close_instruction( + associated_token_account, + spl_token_account_keypair.pubkey(), + transfer_amount, + recipient.pubkey(), + mint, + payer.pubkey(), + token_pool_pda, + token_pool_pda_bump, + anchor_spl::token::ID, + ) + .unwrap(); + + // Execute transaction + rpc.create_and_send_transaction(&[transfer_ix], &payer.pubkey(), &[&payer, &recipient]) + .await + .unwrap(); + + // Verify final balances + { + // Verify SPL token balance is restored + let spl_account_data = rpc + .get_account(spl_token_account_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let spl_account = spl_pod::bytemuck::pod_from_bytes::(&spl_account_data.data) + .map_err(|e| { + RpcError::AssertRpcError(format!("Failed to parse SPL token account: {}", e)) + }) + .unwrap(); + let restored_spl_balance: u64 = spl_account.amount.into(); + assert_eq!( + restored_spl_balance, amount, + "SPL token balance should be restored to original amount" + ); + } + + { + // Verify CToken account is CLOSED (not just balance = 0) + let ctoken_account_result = rpc.get_account(associated_token_account).await.unwrap(); + match ctoken_account_result { + None => { + println!("✓ CToken account successfully closed (account does not exist)"); + } + Some(account_data) => { + assert_eq!( + account_data.data.len(), + 0, + "CToken account data should be empty after CompressAndClose" + ); + assert_eq!( + account_data.lamports, 0, + "CToken account lamports should be 0 after CompressAndClose" + ); + println!("✓ CToken account successfully closed (zeroed out)"); + } + } + } + + println!("✓ Successfully completed CToken -> SPL transfer with CompressAndClose"); + println!(" This validates owner can use CompressAndClose without explicit compressed_token_account validation"); +} diff --git a/program-tests/registry-test/tests/compressible.rs b/program-tests/registry-test/tests/compressible.rs index 327aba5d4d..cf3b18842d 100644 --- a/program-tests/registry-test/tests/compressible.rs +++ b/program-tests/registry-test/tests/compressible.rs @@ -20,7 +20,7 @@ use light_test_utils::{ airdrop_lamports, assert_claim::assert_claim, spl::create_mint_helper, Rpc, RpcError, }; use light_token_client::actions::{ - create_compressible_token_account, CreateCompressibleTokenAccountInputs, + create_compressible_token_account, transfer_ctoken, CreateCompressibleTokenAccountInputs, }; use solana_sdk::{ instruction::Instruction, @@ -1140,7 +1140,6 @@ async fn assert_not_compressible( #[tokio::test] async fn test_compressible_account_infinite_funding() -> Result<(), RpcError> { use light_test_utils::assert_ctoken_transfer::assert_ctoken_transfer; - use light_token_client::actions::ctoken_transfer; let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2(false, None)) .await @@ -1281,7 +1280,7 @@ async fn test_compressible_account_infinite_funding() -> Result<(), RpcError> { }; // Transfer all tokens from source to dest - ctoken_transfer( + transfer_ctoken( &mut rpc, source, dest, diff --git a/programs/compressed-token/anchor/src/lib.rs b/programs/compressed-token/anchor/src/lib.rs index 8faf5ff123..b442ca4b55 100644 --- a/programs/compressed-token/anchor/src/lib.rs +++ b/programs/compressed-token/anchor/src/lib.rs @@ -424,6 +424,10 @@ pub enum ErrorCode { MintActionInvalidCpiContextAddressTreePubkey, #[msg("CompressAndClose: Cannot use the same compressed output account for multiple closures")] CompressAndCloseDuplicateOutput, + #[msg( + "CompressAndClose by compression authority requires compressed token account in outputs" + )] + CompressAndCloseOutputMissing, } impl From for ProgramError { diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs index 78abf4ff8b..6ecafe4a13 100644 --- a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs +++ b/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs @@ -50,10 +50,13 @@ pub fn process_compress_and_close( if compression_authority_is_signer { // Compress the complete balance to this compressed token account. + let compressed_account = close_inputs + .compressed_token_account + .ok_or(ErrorCode::CompressAndCloseOutputMissing)?; validate_compressed_token_account( packed_accounts, amount, - close_inputs.compressed_token_account, + compressed_account, ctoken, compress_to_pubkey, token_account_info.key(), diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/inputs.rs b/programs/compressed-token/program/src/transfer2/compression/ctoken/inputs.rs index 7dd3863dfe..4d54b9e062 100644 --- a/programs/compressed-token/program/src/transfer2/compression/ctoken/inputs.rs +++ b/programs/compressed-token/program/src/transfer2/compression/ctoken/inputs.rs @@ -1,10 +1,7 @@ use light_account_checks::packed_accounts::ProgramPackedAccounts; -use light_ctoken_types::{ - instructions::transfer2::{ - ZCompressedTokenInstructionDataTransfer2, ZCompression, ZCompressionMode, - ZMultiTokenTransferOutputData, - }, - CTokenError, +use light_ctoken_types::instructions::transfer2::{ + ZCompressedTokenInstructionDataTransfer2, ZCompression, ZCompressionMode, + ZMultiTokenTransferOutputData, }; use pinocchio::{account_info::AccountInfo, pubkey::Pubkey}; @@ -12,7 +9,7 @@ use pinocchio::{account_info::AccountInfo, pubkey::Pubkey}; pub struct CompressAndCloseInputs<'a> { pub destination: &'a AccountInfo, pub rent_sponsor: &'a AccountInfo, - pub compressed_token_account: &'a ZMultiTokenTransferOutputData<'a>, + pub compressed_token_account: Option<&'a ZMultiTokenTransferOutputData<'a>>, } /// Input struct for ctoken compression/decompression operations @@ -60,8 +57,7 @@ impl<'a> CTokenCompressionInputs<'a> { )?, compressed_token_account: inputs .out_token_data - .get(compression.get_compressed_token_account_index()? as usize) - .ok_or(CTokenError::AccountFrozen)?, + .get(compression.get_compressed_token_account_index()? as usize), }) } else { None diff --git a/programs/compressed-token/program/src/transfer2/compression/spl.rs b/programs/compressed-token/program/src/transfer2/compression/spl.rs index 781fa92ebf..425d1e20b5 100644 --- a/programs/compressed-token/program/src/transfer2/compression/spl.rs +++ b/programs/compressed-token/program/src/transfer2/compression/spl.rs @@ -77,13 +77,6 @@ fn spl_token_transfer_invoke_cpi( cpi_authority: &AccountInfo, amount: u64, ) -> Result<(), ProgramError> { - msg!("spl_token_transfer_invoke_cpi"); - msg!( - "from {:?}", - solana_pubkey::Pubkey::new_from_array(*from.key()) - ); - msg!("to {:?}", solana_pubkey::Pubkey::new_from_array(*to.key())); - msg!("amount {:?}", amount); let bump_seed = [BUMP_CPI_AUTHORITY]; let seed_array = [ Seed::from(CPI_AUTHORITY_PDA_SEED), @@ -110,13 +103,6 @@ fn spl_token_transfer_invoke( authority: &AccountInfo, amount: u64, ) -> Result<(), ProgramError> { - msg!("spl_token_transfer_invoke"); - msg!( - "from {:?}", - solana_pubkey::Pubkey::new_from_array(*from.key()) - ); - msg!("to {:?}", solana_pubkey::Pubkey::new_from_array(*to.key())); - msg!("amount {:?}", amount); spl_token_transfer_common(program_id, from, to, authority, amount, None) } diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs index ade4b9ee1c..a1f016526d 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mod.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mod.rs @@ -54,8 +54,8 @@ pub use mint_to_compressed::{ }; pub use transfer_ctoken::{transfer_ctoken, transfer_ctoken_signed}; pub use transfer_interface::{ - create_transfer_ctoken_to_spl_instruction, create_transfer_spl_to_ctoken_instruction, - transfer_interface, transfer_interface_signed, + create_ctoken_to_spl_transfer_and_close_instruction, create_transfer_ctoken_to_spl_instruction, + create_transfer_spl_to_ctoken_instruction, transfer_interface, transfer_interface_signed, }; pub use update_compressed_mint::{ update_compressed_mint, update_compressed_mint_cpi, UpdateCompressedMintInputs, diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs index 414d5012ff..1a0cbe2175 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs @@ -159,6 +159,82 @@ pub fn create_transfer_ctoken_to_spl_instruction( create_transfer2_instruction(inputs) } +#[allow(clippy::too_many_arguments)] +#[profile] +pub fn create_ctoken_to_spl_transfer_and_close_instruction( + source_ctoken_account: Pubkey, + destination_spl_token_account: Pubkey, + amount: u64, + authority: Pubkey, + mint: Pubkey, + payer: Pubkey, + token_pool_pda: Pubkey, + token_pool_pda_bump: u8, + spl_token_program: Pubkey, +) -> Result { + let packed_accounts = vec![ + // Mint (index 0) + AccountMeta::new_readonly(mint, false), + // Source ctoken account (index 1) - writable + AccountMeta::new(source_ctoken_account, false), + // Destination SPL token account (index 2) - writable + AccountMeta::new(destination_spl_token_account, false), + // Authority (index 3) - signer + AccountMeta::new(authority, true), + // Token pool PDA (index 4) - writable + AccountMeta::new(token_pool_pda, false), + // SPL Token program (index 5) - needed for CPI + AccountMeta::new_readonly(spl_token_program, false), + ]; + + // First operation: compress from ctoken account to pool using compress_and_close + let compress_to_pool = CTokenAccount2 { + inputs: vec![], + output: MultiTokenTransferOutputData::default(), + compression: Some(Compression::compress_and_close_ctoken( + amount, 0, // mint index + 1, // source ctoken account index + 3, // authority index + 0, // no rent sponsor + 0, // no compressed account + 3, // destination is authority + )), + delegate_is_set: false, + method_used: true, + }; + + // Second operation: decompress from pool to SPL token account using decompress_spl + let decompress_to_spl = CTokenAccount2 { + inputs: vec![], + output: MultiTokenTransferOutputData::default(), + compression: Some(Compression::decompress_spl( + amount, + 0, // mint index + 2, // destination SPL token account index + 4, // pool_account_index + 0, // pool_index (TODO: make dynamic) + token_pool_pda_bump, + )), + delegate_is_set: false, + method_used: true, + }; + + let inputs = Transfer2Inputs { + validity_proof: ValidityProof::new(None), + transfer_config: Transfer2Config::default().filter_zero_amount_outputs(), + meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only( + payer, + packed_accounts, + ), + in_lamports: None, + out_lamports: None, + token_accounts: vec![compress_to_pool, decompress_to_spl], + output_queue: 0, // Decompressed accounts only, no output queue needed + }; + + create_transfer2_instruction(inputs) +} + /// Transfer SPL tokens to compressed tokens #[allow(clippy::too_many_arguments)] pub fn transfer_spl_to_ctoken<'info>(