diff --git a/js/stateless.js/src/rpc-interface.ts b/js/stateless.js/src/rpc-interface.ts index f5312d3a63..c6963bc132 100644 --- a/js/stateless.js/src/rpc-interface.ts +++ b/js/stateless.js/src/rpc-interface.ts @@ -114,7 +114,6 @@ export interface HexInputsForProver { leaf: string; } -// TODO: Rename Compressed -> ValidityProof export type CompressedProofWithContext = { compressedProof: CompressedProof; roots: BN[]; diff --git a/sdk-libs/compressed-token-client/examples/basic_usage.rs b/sdk-libs/compressed-token-client/examples/basic_usage.rs deleted file mode 100644 index 6e31e33686..0000000000 --- a/sdk-libs/compressed-token-client/examples/basic_usage.rs +++ /dev/null @@ -1,135 +0,0 @@ -//! Example demonstrating basic usage of the compressed-token-client library - -use light_compressed_token_client::{ - batch_compress, compress, create_decompress_instruction, AccountState, CompressedAccount, - DecompressParams, MerkleContext, TokenData, TreeType, -}; -use solana_sdk::pubkey::Pubkey; - -fn main() { - // Example 1: Simple compression - simple_compress_example(); - - // Example 2: Batch compression - batch_compress_example(); - - // Example 3: Decompression - decompress_example(); -} - -fn simple_compress_example() { - println!("=== Simple Compress Example ==="); - - let payer = Pubkey::new_unique(); - let owner = Pubkey::new_unique(); - let source_token_account = Pubkey::new_unique(); - let mint = Pubkey::new_unique(); - let recipient = Pubkey::new_unique(); - let output_state_tree = Pubkey::new_unique(); - - let instruction = compress( - payer, - owner, - source_token_account, - mint, - 1000, // amount - recipient, - output_state_tree, - ) - .expect("Failed to create compress instruction"); - - println!("Created compress instruction:"); - println!(" Program ID: {}", instruction.program_id); - println!(" Accounts: {} total", instruction.accounts.len()); - println!(); -} - -fn batch_compress_example() { - println!("=== Batch Compress Example ==="); - - let payer = Pubkey::new_unique(); - let owner = Pubkey::new_unique(); - let source_token_account = Pubkey::new_unique(); - let mint = Pubkey::new_unique(); - let output_state_tree = Pubkey::new_unique(); - - let recipients = vec![ - (Pubkey::new_unique(), 500), - (Pubkey::new_unique(), 300), - (Pubkey::new_unique(), 200), - ]; - - let _instruction = batch_compress( - payer, - owner, - source_token_account, - mint, - recipients.clone(), - output_state_tree, - ) - .expect("Failed to create batch compress instruction"); - - println!( - "Created batch compress instruction for {} recipients:", - recipients.len() - ); - for (recipient, amount) in recipients { - println!(" {} -> {} tokens", recipient, amount); - } - println!(); -} - -fn decompress_example() { - println!("=== Decompress Example ==="); - - let payer = Pubkey::new_unique(); - let owner = Pubkey::new_unique(); - let mint = Pubkey::new_unique(); - let destination_token_account = Pubkey::new_unique(); - let merkle_tree = Pubkey::new_unique(); - let queue = Pubkey::new_unique(); - - // Create mock compressed account data - let compressed_account = CompressedAccount { - owner: light_compressed_token_client::PROGRAM_ID, - lamports: 0, - data: None, - address: None, - }; - - let token_data = TokenData { - mint, - owner, - amount: 1000, - delegate: None, - state: AccountState::Initialized, - tlv: None, - }; - - let merkle_context = MerkleContext { - merkle_tree_pubkey: merkle_tree, - queue_pubkey: queue, - leaf_index: 0, - prove_by_index: false, - tree_type: TreeType::StateV2, - }; - - let params = DecompressParams { - payer, - input_compressed_token_accounts: vec![(compressed_account, token_data, merkle_context)], - to_address: destination_token_account, - amount: 500, - recent_input_state_root_indices: vec![Some(0)], - recent_validity_proof: None, - output_state_tree: Some(merkle_tree), - token_program_id: None, - }; - - let instruction = - create_decompress_instruction(params).expect("Failed to create decompress instruction"); - - println!("Created decompress instruction:"); - println!(" Program ID: {}", instruction.program_id); - println!(" Decompressing 500 tokens (500 remain compressed)"); - println!(); -} diff --git a/sdk-libs/compressed-token-client/src/instructions.rs b/sdk-libs/compressed-token-client/src/instructions.rs index c50ca8ceeb..5b160962f6 100644 --- a/sdk-libs/compressed-token-client/src/instructions.rs +++ b/sdk-libs/compressed-token-client/src/instructions.rs @@ -62,19 +62,18 @@ pub struct DecompressParams { /// Create a compress instruction /// -/// This instruction transfers tokens from an SPL token account to compressed token accounts. +/// This instruction compresses tokens from an SPL token account to N recipients. pub fn create_compress_instruction( params: CompressParams, ) -> Result { let token_program = params.token_program_id.unwrap_or(anchor_spl::token::ID); - // Create output compressed accounts - let output_compressed_accounts = if let Some(batch_recipients) = params.batch_recipients { + let output_compressed_accounts = if let Some(ref batch_recipients) = params.batch_recipients { batch_recipients - .into_iter() + .iter() .map(|(recipient, amount)| TokenTransferOutputData { - owner: recipient, - amount, + owner: *recipient, + amount: *amount, lamports: None, merkle_tree: params.output_state_tree, }) @@ -87,41 +86,46 @@ pub fn create_compress_instruction( merkle_tree: params.output_state_tree, }] }; - - // Calculate total amount let total_amount: u64 = output_compressed_accounts.iter().map(|x| x.amount).sum(); - // Create the instruction using the transfer SDK - transfer_sdk::create_transfer_instruction( + // TODO: refactor. + let ix = match transfer_sdk::create_transfer_instruction( ¶ms.payer, ¶ms.owner, - &[], // empty input merkle context for compression + &[], &output_compressed_accounts, - &[], // empty root indices for compression - &None, // no proof needed for compression - &[], // empty input token data for compression - &[], // empty input compressed accounts for compression + &[], + &None, + &[], + &[], params.mint, - None, // no delegate - true, // is_compress = true - Some(total_amount), // compress_or_decompress_amount - Some(crate::get_token_pool_pda(¶ms.mint)), // token_pool_pda - Some(params.source), // compress_or_decompress_token_account - false, // don't sort outputs - None, // no delegate change account - None, // no lamports change account - token_program == spl_token_2022::ID, // is_token_22 - &[], // no additional token pools - false, // with_transaction_hash = false - ) - .map_err(|e| { - CompressedTokenError::SerializationError(format!("Failed to create instruction: {:?}", e)) - }) + None, + true, + Some(total_amount), + Some(crate::get_token_pool_pda(¶ms.mint)), + Some(params.source), + false, + None, + None, + token_program == spl_token_2022::ID, + &[], + false, + ) { + Ok(ix) => ix, + Err(e) => { + return Err(CompressedTokenError::SerializationError(format!( + "Failed to create instruction: {:?}", + e + ))) + } + }; + + Ok(ix) } /// Create a decompress instruction /// -/// This instruction transfers tokens from compressed token accounts to an SPL token account. +/// This instruction decompresses compressed tokens to an SPL token account. pub fn create_decompress_instruction( params: DecompressParams, ) -> Result { @@ -133,7 +137,6 @@ pub fn create_decompress_instruction( let token_program = params.token_program_id.unwrap_or(anchor_spl::token::ID); - // Extract components from input accounts let (compressed_accounts, token_data, merkle_contexts): (Vec<_>, Vec<_>, Vec<_>) = params .input_compressed_token_accounts .into_iter() @@ -148,13 +151,9 @@ pub fn create_decompress_instruction( }, ); - // Get mint from first token data let mint = token_data[0].mint; - - // Get owner from first token data let owner = token_data[0].owner; - // Create output state for remaining tokens (if any) let input_total: u64 = token_data.iter().map(|td| td.amount).sum(); let remaining_amount = input_total.saturating_sub(params.amount); @@ -171,7 +170,7 @@ pub fn create_decompress_instruction( vec![] }; - // Create the instruction using the transfer SDK + // TODO: refactor. transfer_sdk::create_transfer_instruction( ¶ms.payer, &owner, @@ -182,26 +181,24 @@ pub fn create_decompress_instruction( &token_data, &compressed_accounts, mint, - None, // no delegate - false, // is_compress = false - Some(params.amount), // compress_or_decompress_amount - Some(crate::get_token_pool_pda(&mint)), // token_pool_pda - Some(params.to_address), // compress_or_decompress_token_account - false, // don't sort outputs - None, // no delegate change account - None, // no lamports change account - token_program == spl_token_2022::ID, // is_token_22 - &[], // no additional token pools - false, // with_transaction_hash = false + None, + false, + Some(params.amount), + Some(crate::get_token_pool_pda(&mint)), + Some(params.to_address), + false, + None, + None, + token_program == spl_token_2022::ID, + &[], + false, ) .map_err(|e| { CompressedTokenError::SerializationError(format!("Failed to create instruction: {:?}", e)) }) } -/// Helper function to create a simple compress instruction -/// -/// This is a convenience function for the most common compress use case. +/// Create a compress instruction with a single recipient. pub fn compress( payer: Pubkey, owner: Pubkey, @@ -224,27 +221,32 @@ pub fn compress( }) } -/// Helper function to create a batch compress instruction -/// -/// Compress tokens to multiple recipients in a single transaction. +/// Creates a compress instruction to compress tokens to multiple recipients. pub fn batch_compress( payer: Pubkey, owner: Pubkey, source_token_account: Pubkey, mint: Pubkey, - recipients: Vec<(Pubkey, u64)>, + recipients: Vec, + amounts: Vec, output_state_tree: Pubkey, ) -> Result { + if recipients.len() != amounts.len() { + return Err(CompressedTokenError::InvalidParams( + "Recipients and amounts must have the same length".to_string(), + )); + } + create_compress_instruction(CompressParams { payer, owner, source: source_token_account, - to_address: Pubkey::default(), // Not used in batch mode + to_address: Pubkey::default(), mint, - amount: 0, // Not used in batch mode + amount: 0, output_state_tree, token_program_id: None, - batch_recipients: Some(recipients), + batch_recipients: Some(recipients.into_iter().zip(amounts).collect()), }) } @@ -286,12 +288,21 @@ mod tests { let output_state_tree = Pubkey::new_unique(); let recipients = vec![ - (Pubkey::new_unique(), 500), - (Pubkey::new_unique(), 300), - (Pubkey::new_unique(), 200), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), ]; + let amounts = vec![500, 300, 200]; - let result = batch_compress(payer, owner, source, mint, recipients, output_state_tree); + let result = batch_compress( + payer, + owner, + source, + mint, + recipients, + amounts, + output_state_tree, + ); assert!(result.is_ok()); let instruction = result.unwrap(); diff --git a/sdk-libs/compressed-token-client/tests/integration_test.rs b/sdk-libs/compressed-token-client/tests/integration_test.rs new file mode 100644 index 0000000000..ab9d1695ab --- /dev/null +++ b/sdk-libs/compressed-token-client/tests/integration_test.rs @@ -0,0 +1,225 @@ +#[cfg(test)] +mod tests { + use light_compressed_token_client::{ + batch_compress, compress, create_decompress_instruction, AccountState, CompressedAccount, + DecompressParams, MerkleContext, TokenData, TreeType, + }; + use solana_sdk::pubkey::Pubkey; + + #[test] + fn test_simple_compress() { + let payer = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let source_token_account = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let recipient = Pubkey::new_unique(); + let output_state_tree = Pubkey::new_unique(); + + let instruction = compress( + payer, + owner, + source_token_account, + mint, + 1000, // amount + recipient, + output_state_tree, + ) + .expect("Failed to create compress instruction"); + + assert_eq!( + instruction.program_id, + light_compressed_token_client::PROGRAM_ID + ); + assert!(!instruction.accounts.is_empty()); + + let account_keys: Vec<_> = instruction.accounts.iter().map(|a| a.pubkey).collect(); + assert!(account_keys.contains(&payer)); + assert!(account_keys.contains(&owner)); + assert!(account_keys.contains(&source_token_account)); + assert!(account_keys.contains(&output_state_tree)); + } + + #[test] + fn test_batch_compress() { + let payer = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let source_token_account = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let output_state_tree = Pubkey::new_unique(); + + let recipients = vec![ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + let amounts = vec![500, 300, 200]; + + let total_amount: u64 = amounts.iter().sum(); + assert_eq!(total_amount, 1000); + + let instruction = batch_compress( + payer, + owner, + source_token_account, + mint, + recipients, + amounts, + output_state_tree, + ) + .expect("Failed to create batch compress instruction"); + + assert_eq!( + instruction.program_id, + light_compressed_token_client::PROGRAM_ID + ); + assert!(!instruction.accounts.is_empty()); + + let account_keys: Vec<_> = instruction.accounts.iter().map(|a| a.pubkey).collect(); + assert!(account_keys.contains(&payer)); + assert!(account_keys.contains(&owner)); + assert!(account_keys.contains(&source_token_account)); + + assert!(account_keys.contains(&output_state_tree)); + } + + #[test] + fn test_decompress() { + let payer = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let destination_token_account = Pubkey::new_unique(); + let merkle_tree = Pubkey::new_unique(); + let queue = Pubkey::new_unique(); + + let compressed_account = CompressedAccount { + owner: light_compressed_token_client::PROGRAM_ID, + lamports: 0, + data: None, + address: None, + }; + + let token_data = TokenData { + mint, + owner, + amount: 1000, + delegate: None, + state: AccountState::Initialized, + tlv: None, + }; + + let merkle_context = MerkleContext { + merkle_tree_pubkey: merkle_tree, + queue_pubkey: queue, + leaf_index: 0, + prove_by_index: false, + tree_type: TreeType::StateV2, + }; + + let params = DecompressParams { + payer, + input_compressed_token_accounts: vec![( + compressed_account.clone(), + token_data.clone(), + merkle_context.clone(), + )], + to_address: destination_token_account, + amount: 500, + recent_input_state_root_indices: vec![Some(0)], + recent_validity_proof: None, + output_state_tree: Some(merkle_tree), + token_program_id: None, + }; + + let instruction = + create_decompress_instruction(params).expect("Failed to create decompress instruction"); + + assert_eq!( + instruction.program_id, + light_compressed_token_client::PROGRAM_ID + ); + assert!(!instruction.accounts.is_empty()); + + let account_keys: Vec<_> = instruction.accounts.iter().map(|a| a.pubkey).collect(); + assert!(account_keys.contains(&payer)); + assert!(account_keys.contains(&destination_token_account)); + assert!(account_keys.contains(&merkle_tree)); + assert!(account_keys.contains(&queue)); + + assert_eq!(token_data.amount, 1000); + assert_eq!(token_data.owner, owner); + assert_eq!(token_data.mint, mint); + assert_eq!(token_data.state, AccountState::Initialized); + + assert_eq!( + compressed_account.owner, + light_compressed_token_client::PROGRAM_ID + ); + assert_eq!(compressed_account.lamports, 0); + + assert_eq!(merkle_context.merkle_tree_pubkey, merkle_tree); + assert_eq!(merkle_context.queue_pubkey, queue); + assert_eq!(merkle_context.leaf_index, 0); + assert!(!merkle_context.prove_by_index); + assert_eq!(merkle_context.tree_type, TreeType::StateV2); + } + + #[test] + fn test_decompress_partial_amount() { + let payer = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let destination_token_account = Pubkey::new_unique(); + let merkle_tree = Pubkey::new_unique(); + let queue = Pubkey::new_unique(); + + let total_amount = 1000u64; + let decompress_amount = 500u64; + + let compressed_account = CompressedAccount { + owner: light_compressed_token_client::PROGRAM_ID, + lamports: 0, + data: None, + address: None, + }; + + let token_data = TokenData { + mint, + owner, + amount: total_amount, + delegate: None, + state: AccountState::Initialized, + tlv: None, + }; + + let merkle_context = MerkleContext { + merkle_tree_pubkey: merkle_tree, + queue_pubkey: queue, + leaf_index: 0, + prove_by_index: false, + tree_type: TreeType::StateV2, + }; + + let params = DecompressParams { + payer, + input_compressed_token_accounts: vec![( + compressed_account, + token_data.clone(), + merkle_context, + )], + to_address: destination_token_account, + amount: decompress_amount, + recent_input_state_root_indices: vec![Some(0)], + recent_validity_proof: None, + output_state_tree: Some(merkle_tree), + token_program_id: None, + }; + + let instruction = + create_decompress_instruction(params).expect("Failed to create decompress instruction"); + + assert!(instruction.accounts.len() > 0); + assert_eq!(token_data.amount, total_amount); + assert!(decompress_amount < total_amount); + assert_eq!(total_amount - decompress_amount, 500); + } +} diff --git a/sdk-libs/compressed-token-sdk/src/cpi/account_info.rs b/sdk-libs/compressed-token-sdk/src/cpi/account_info.rs index be5526b5ff..22d6a9d128 100644 --- a/sdk-libs/compressed-token-sdk/src/cpi/account_info.rs +++ b/sdk-libs/compressed-token-sdk/src/cpi/account_info.rs @@ -1,6 +1,6 @@ +use crate::state::InputTokenDataWithContext; use light_compressed_account::compressed_account::PackedMerkleContext; -use crate::state::InputTokenDataWithContext; /// Get an existing compressed token account from token_data in optimized /// format. /// diff --git a/sdk-libs/compressed-token-sdk/src/cpi/instruction.rs b/sdk-libs/compressed-token-sdk/src/cpi/instruction.rs index c097fad72c..4bc94c6386 100644 --- a/sdk-libs/compressed-token-sdk/src/cpi/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/cpi/instruction.rs @@ -1,3 +1,4 @@ +use crate::cpi::accounts::CompressedTokenDecompressCpiAccounts; #[cfg(feature = "anchor")] use anchor_lang::AnchorSerialize; #[cfg(not(feature = "anchor"))] @@ -11,10 +12,7 @@ use solana_program::{ pubkey::Pubkey, }; -use crate::{ - cpi::accounts::CompressedTokenDecompressCpiAccounts, - state::{CompressedTokenInstructionDataTransfer, InputTokenDataWithContext}, -}; +use crate::state::{CompressedTokenInstructionDataTransfer, InputTokenDataWithContext}; /// Return Instruction to decompress compressed token accounts. /// Proof can be None if prove_by_index is used. @@ -31,7 +29,7 @@ pub fn decompress( let accounts = vec![ AccountMeta::new(*light_cpi_accounts.fee_payer.key, true), AccountMeta::new_readonly(*light_cpi_accounts.authority.key, true), - AccountMeta::new_readonly(*light_cpi_accounts.cpi_authority_pda.key, true), + AccountMeta::new_readonly(*light_cpi_accounts.cpi_authority_pda.key, false), AccountMeta::new_readonly(*light_cpi_accounts.light_system_program.key, false), AccountMeta::new_readonly(*light_cpi_accounts.registered_program_pda.key, false), AccountMeta::new_readonly(*light_cpi_accounts.noop_program.key, false), @@ -47,7 +45,7 @@ pub fn decompress( ]; Ok(Instruction { - program_id: *light_cpi_accounts.token_program.key, + program_id: *light_cpi_accounts.self_program.key, accounts, data, }) @@ -75,12 +73,20 @@ pub fn decompress_token_instruction_data( compress_or_decompress_amount: Some(amount), cpi_context: cpi_context.copied(), lamports_change_account_merkle_tree_index: None, + with_transaction_hash: false, }; let mut inputs = Vec::new(); + // transfer discriminator + inputs.extend_from_slice(&[163, 52, 200, 231, 140, 3, 69, 186]); + let mut serialized_data = Vec::new(); compressed_token_instruction_data_transfer - .serialize(&mut inputs) + .serialize(&mut serialized_data) .unwrap(); + + // Add length buffer + inputs.extend_from_slice(&(serialized_data.len() as u32).to_le_bytes()); + inputs.extend_from_slice(&serialized_data); inputs } diff --git a/sdk-libs/compressed-token-sdk/src/state.rs b/sdk-libs/compressed-token-sdk/src/state.rs index ed32b34f71..c4abdec1db 100644 --- a/sdk-libs/compressed-token-sdk/src/state.rs +++ b/sdk-libs/compressed-token-sdk/src/state.rs @@ -6,8 +6,19 @@ use light_compressed_account::{ compressed_account::{CompressedAccountWithMerkleContext, PackedMerkleContext}, instruction_data::{compressed_proof::CompressedProof, cpi_context::CompressedCpiContext}, }; + use solana_program::pubkey::Pubkey; +#[derive(Clone, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] +pub struct PackedTokenTransferOutputData { + pub owner: Pubkey, + pub amount: u64, + pub lamports: Option, + pub merkle_tree_index: u8, + /// Placeholder for TokenExtension tlv data (unimplemented) + pub tlv: Option>, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] #[repr(u8)] pub enum AccountState { @@ -42,14 +53,17 @@ pub struct TokenDataWithMerkleContext { pub struct CompressedTokenInstructionDataTransfer { pub proof: Option, pub mint: Pubkey, - - pub delegated_transfer: Option, + /// Is required if the signer is delegate, + /// -> delegate is authority account, + /// owner = Some(owner) is the owner of the token account. + pub delegated_transfer: Option, pub input_token_data_with_context: Vec, - pub output_compressed_accounts: Vec, + pub output_compressed_accounts: Vec, pub is_compress: bool, pub compress_or_decompress_amount: Option, pub cpi_context: Option, pub lamports_change_account_merkle_tree_index: Option, + pub with_transaction_hash: bool, } #[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)]