diff --git a/client/tests/rpc_client.rs b/client/tests/rpc_client.rs index 5096df998..2b1c659d8 100644 --- a/client/tests/rpc_client.rs +++ b/client/tests/rpc_client.rs @@ -139,6 +139,7 @@ async fn test_all_endpoints() { vec![payer_pubkey], None, false, + 0, ); let tx = Transaction::new_signed_with_payer( diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/sdk.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/sdk.rs index 524188ac5..695214e49 100644 --- a/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/sdk.rs +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_pda/sdk.rs @@ -130,6 +130,7 @@ pub fn create_withdrawal_escrow_instruction( None, None, None, + &[], ); let merkle_tree_indices = add_and_get_remaining_account_indices( diff --git a/js/compressed-token/src/idl/light_compressed_token.ts b/js/compressed-token/src/idl/light_compressed_token.ts index 623efed8c..72a140899 100644 --- a/js/compressed-token/src/idl/light_compressed_token.ts +++ b/js/compressed-token/src/idl/light_compressed_token.ts @@ -45,6 +45,57 @@ export type LightCompressedToken = { ]; args: []; }, + { + name: 'addTokenPool'; + docs: [ + 'This instruction creates an additional token pool for a given mint.', + 'The maximum number of token pools per mint is 5.', + ]; + accounts: [ + { + name: 'feePayer'; + isMut: true; + isSigner: true; + docs: ['UNCHECKED: only pays fees.']; + }, + { + name: 'tokenPoolPda'; + isMut: true; + isSigner: false; + }, + { + name: 'existingTokenPoolPda'; + isMut: false; + isSigner: false; + }, + { + name: 'systemProgram'; + isMut: false; + isSigner: false; + }, + { + name: 'mint'; + isMut: true; + isSigner: false; + }, + { + name: 'tokenProgram'; + isMut: false; + isSigner: false; + }, + { + name: 'cpiAuthorityPda'; + isMut: false; + isSigner: false; + }, + ]; + args: [ + { + name: 'tokenPoolBump'; + type: 'u8'; + }, + ]; + }, { name: 'mintTo'; docs: [ @@ -1530,141 +1581,23 @@ export type LightCompressedToken = { errors: [ { code: 6000; - name: 'PublicKeyAmountMissmatch'; - msg: 'public keys and amounts must be of same length'; + name: 'SignerCheckFailed'; + msg: 'Signer check failed'; }, { code: 6001; - name: 'ComputeInputSumFailed'; - msg: 'ComputeInputSumFailed'; + name: 'CreateTransferInstructionFailed'; + msg: 'Create transfer instruction failed'; }, { code: 6002; - name: 'ComputeOutputSumFailed'; - msg: 'ComputeOutputSumFailed'; + name: 'AccountNotFound'; + msg: 'Account not found'; }, { code: 6003; - name: 'ComputeCompressSumFailed'; - msg: 'ComputeCompressSumFailed'; - }, - { - code: 6004; - name: 'ComputeDecompressSumFailed'; - msg: 'ComputeDecompressSumFailed'; - }, - { - code: 6005; - name: 'SumCheckFailed'; - msg: 'SumCheckFailed'; - }, - { - code: 6006; - name: 'DecompressRecipientUndefinedForDecompress'; - msg: 'DecompressRecipientUndefinedForDecompress'; - }, - { - code: 6007; - name: 'CompressedPdaUndefinedForDecompress'; - msg: 'CompressedPdaUndefinedForDecompress'; - }, - { - code: 6008; - name: 'DeCompressAmountUndefinedForDecompress'; - msg: 'DeCompressAmountUndefinedForDecompress'; - }, - { - code: 6009; - name: 'CompressedPdaUndefinedForCompress'; - msg: 'CompressedPdaUndefinedForCompress'; - }, - { - code: 6010; - name: 'DeCompressAmountUndefinedForCompress'; - msg: 'DeCompressAmountUndefinedForCompress'; - }, - { - code: 6011; - name: 'DelegateSignerCheckFailed'; - msg: 'DelegateSignerCheckFailed'; - }, - { - code: 6012; - name: 'MintTooLarge'; - msg: 'Minted amount greater than u64::MAX'; - }, - { - code: 6013; - name: 'SplTokenSupplyMismatch'; - msg: 'SplTokenSupplyMismatch'; - }, - { - code: 6014; - name: 'HeapMemoryCheckFailed'; - msg: 'HeapMemoryCheckFailed'; - }, - { - code: 6015; - name: 'InstructionNotCallable'; - msg: 'The instruction is not callable'; - }, - { - code: 6016; - name: 'ArithmeticUnderflow'; - msg: 'ArithmeticUnderflow'; - }, - { - code: 6017; - name: 'HashToFieldError'; - msg: 'HashToFieldError'; - }, - { - code: 6018; - name: 'InvalidAuthorityMint'; - msg: 'Expected the authority to be also a mint authority'; - }, - { - code: 6019; - name: 'InvalidFreezeAuthority'; - msg: 'Provided authority is not the freeze authority'; - }, - { - code: 6020; - name: 'InvalidDelegateIndex'; - }, - { - code: 6021; - name: 'TokenPoolPdaUndefined'; - }, - { - code: 6022; - name: 'IsTokenPoolPda'; - msg: 'Compress or decompress recipient is the same account as the token pool pda.'; - }, - { - code: 6023; - name: 'InvalidTokenPoolPda'; - }, - { - code: 6024; - name: 'NoInputTokenAccountsProvided'; - }, - { - code: 6025; - name: 'NoInputsProvided'; - }, - { - code: 6026; - name: 'MintHasNoFreezeAuthority'; - }, - { - code: 6027; - name: 'MintWithInvalidExtension'; - }, - { - code: 6028; - name: 'InsufficientTokenAccountBalance'; - msg: 'The token account balance is less than the remaining amount.'; + name: 'SerializationError'; + msg: 'Serialization error'; }, ]; }; @@ -1715,6 +1648,57 @@ export const IDL: LightCompressedToken = { ], args: [], }, + { + name: 'addTokenPool', + docs: [ + 'This instruction creates an additional token pool for a given mint.', + 'The maximum number of token pools per mint is 5.', + ], + accounts: [ + { + name: 'feePayer', + isMut: true, + isSigner: true, + docs: ['UNCHECKED: only pays fees.'], + }, + { + name: 'tokenPoolPda', + isMut: true, + isSigner: false, + }, + { + name: 'existingTokenPoolPda', + isMut: false, + isSigner: false, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + { + name: 'mint', + isMut: true, + isSigner: false, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + { + name: 'cpiAuthorityPda', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'tokenPoolBump', + type: 'u8', + }, + ], + }, { name: 'mintTo', docs: [ @@ -3205,141 +3189,23 @@ export const IDL: LightCompressedToken = { errors: [ { code: 6000, - name: 'PublicKeyAmountMissmatch', - msg: 'public keys and amounts must be of same length', + name: 'SignerCheckFailed', + msg: 'Signer check failed', }, { code: 6001, - name: 'ComputeInputSumFailed', - msg: 'ComputeInputSumFailed', + name: 'CreateTransferInstructionFailed', + msg: 'Create transfer instruction failed', }, { code: 6002, - name: 'ComputeOutputSumFailed', - msg: 'ComputeOutputSumFailed', + name: 'AccountNotFound', + msg: 'Account not found', }, { code: 6003, - name: 'ComputeCompressSumFailed', - msg: 'ComputeCompressSumFailed', - }, - { - code: 6004, - name: 'ComputeDecompressSumFailed', - msg: 'ComputeDecompressSumFailed', - }, - { - code: 6005, - name: 'SumCheckFailed', - msg: 'SumCheckFailed', - }, - { - code: 6006, - name: 'DecompressRecipientUndefinedForDecompress', - msg: 'DecompressRecipientUndefinedForDecompress', - }, - { - code: 6007, - name: 'CompressedPdaUndefinedForDecompress', - msg: 'CompressedPdaUndefinedForDecompress', - }, - { - code: 6008, - name: 'DeCompressAmountUndefinedForDecompress', - msg: 'DeCompressAmountUndefinedForDecompress', - }, - { - code: 6009, - name: 'CompressedPdaUndefinedForCompress', - msg: 'CompressedPdaUndefinedForCompress', - }, - { - code: 6010, - name: 'DeCompressAmountUndefinedForCompress', - msg: 'DeCompressAmountUndefinedForCompress', - }, - { - code: 6011, - name: 'DelegateSignerCheckFailed', - msg: 'DelegateSignerCheckFailed', - }, - { - code: 6012, - name: 'MintTooLarge', - msg: 'Minted amount greater than u64::MAX', - }, - { - code: 6013, - name: 'SplTokenSupplyMismatch', - msg: 'SplTokenSupplyMismatch', - }, - { - code: 6014, - name: 'HeapMemoryCheckFailed', - msg: 'HeapMemoryCheckFailed', - }, - { - code: 6015, - name: 'InstructionNotCallable', - msg: 'The instruction is not callable', - }, - { - code: 6016, - name: 'ArithmeticUnderflow', - msg: 'ArithmeticUnderflow', - }, - { - code: 6017, - name: 'HashToFieldError', - msg: 'HashToFieldError', - }, - { - code: 6018, - name: 'InvalidAuthorityMint', - msg: 'Expected the authority to be also a mint authority', - }, - { - code: 6019, - name: 'InvalidFreezeAuthority', - msg: 'Provided authority is not the freeze authority', - }, - { - code: 6020, - name: 'InvalidDelegateIndex', - }, - { - code: 6021, - name: 'TokenPoolPdaUndefined', - }, - { - code: 6022, - name: 'IsTokenPoolPda', - msg: 'Compress or decompress recipient is the same account as the token pool pda.', - }, - { - code: 6023, - name: 'InvalidTokenPoolPda', - }, - { - code: 6024, - name: 'NoInputTokenAccountsProvided', - }, - { - code: 6025, - name: 'NoInputsProvided', - }, - { - code: 6026, - name: 'MintHasNoFreezeAuthority', - }, - { - code: 6027, - name: 'MintWithInvalidExtension', - }, - { - code: 6028, - name: 'InsufficientTokenAccountBalance', - msg: 'The token account balance is less than the remaining amount.', + name: 'SerializationError', + msg: 'Serialization error', }, ], }; diff --git a/js/stateless.js/src/idls/light_compressed_token.ts b/js/stateless.js/src/idls/light_compressed_token.ts index 623efed8c..72a140899 100644 --- a/js/stateless.js/src/idls/light_compressed_token.ts +++ b/js/stateless.js/src/idls/light_compressed_token.ts @@ -45,6 +45,57 @@ export type LightCompressedToken = { ]; args: []; }, + { + name: 'addTokenPool'; + docs: [ + 'This instruction creates an additional token pool for a given mint.', + 'The maximum number of token pools per mint is 5.', + ]; + accounts: [ + { + name: 'feePayer'; + isMut: true; + isSigner: true; + docs: ['UNCHECKED: only pays fees.']; + }, + { + name: 'tokenPoolPda'; + isMut: true; + isSigner: false; + }, + { + name: 'existingTokenPoolPda'; + isMut: false; + isSigner: false; + }, + { + name: 'systemProgram'; + isMut: false; + isSigner: false; + }, + { + name: 'mint'; + isMut: true; + isSigner: false; + }, + { + name: 'tokenProgram'; + isMut: false; + isSigner: false; + }, + { + name: 'cpiAuthorityPda'; + isMut: false; + isSigner: false; + }, + ]; + args: [ + { + name: 'tokenPoolBump'; + type: 'u8'; + }, + ]; + }, { name: 'mintTo'; docs: [ @@ -1530,141 +1581,23 @@ export type LightCompressedToken = { errors: [ { code: 6000; - name: 'PublicKeyAmountMissmatch'; - msg: 'public keys and amounts must be of same length'; + name: 'SignerCheckFailed'; + msg: 'Signer check failed'; }, { code: 6001; - name: 'ComputeInputSumFailed'; - msg: 'ComputeInputSumFailed'; + name: 'CreateTransferInstructionFailed'; + msg: 'Create transfer instruction failed'; }, { code: 6002; - name: 'ComputeOutputSumFailed'; - msg: 'ComputeOutputSumFailed'; + name: 'AccountNotFound'; + msg: 'Account not found'; }, { code: 6003; - name: 'ComputeCompressSumFailed'; - msg: 'ComputeCompressSumFailed'; - }, - { - code: 6004; - name: 'ComputeDecompressSumFailed'; - msg: 'ComputeDecompressSumFailed'; - }, - { - code: 6005; - name: 'SumCheckFailed'; - msg: 'SumCheckFailed'; - }, - { - code: 6006; - name: 'DecompressRecipientUndefinedForDecompress'; - msg: 'DecompressRecipientUndefinedForDecompress'; - }, - { - code: 6007; - name: 'CompressedPdaUndefinedForDecompress'; - msg: 'CompressedPdaUndefinedForDecompress'; - }, - { - code: 6008; - name: 'DeCompressAmountUndefinedForDecompress'; - msg: 'DeCompressAmountUndefinedForDecompress'; - }, - { - code: 6009; - name: 'CompressedPdaUndefinedForCompress'; - msg: 'CompressedPdaUndefinedForCompress'; - }, - { - code: 6010; - name: 'DeCompressAmountUndefinedForCompress'; - msg: 'DeCompressAmountUndefinedForCompress'; - }, - { - code: 6011; - name: 'DelegateSignerCheckFailed'; - msg: 'DelegateSignerCheckFailed'; - }, - { - code: 6012; - name: 'MintTooLarge'; - msg: 'Minted amount greater than u64::MAX'; - }, - { - code: 6013; - name: 'SplTokenSupplyMismatch'; - msg: 'SplTokenSupplyMismatch'; - }, - { - code: 6014; - name: 'HeapMemoryCheckFailed'; - msg: 'HeapMemoryCheckFailed'; - }, - { - code: 6015; - name: 'InstructionNotCallable'; - msg: 'The instruction is not callable'; - }, - { - code: 6016; - name: 'ArithmeticUnderflow'; - msg: 'ArithmeticUnderflow'; - }, - { - code: 6017; - name: 'HashToFieldError'; - msg: 'HashToFieldError'; - }, - { - code: 6018; - name: 'InvalidAuthorityMint'; - msg: 'Expected the authority to be also a mint authority'; - }, - { - code: 6019; - name: 'InvalidFreezeAuthority'; - msg: 'Provided authority is not the freeze authority'; - }, - { - code: 6020; - name: 'InvalidDelegateIndex'; - }, - { - code: 6021; - name: 'TokenPoolPdaUndefined'; - }, - { - code: 6022; - name: 'IsTokenPoolPda'; - msg: 'Compress or decompress recipient is the same account as the token pool pda.'; - }, - { - code: 6023; - name: 'InvalidTokenPoolPda'; - }, - { - code: 6024; - name: 'NoInputTokenAccountsProvided'; - }, - { - code: 6025; - name: 'NoInputsProvided'; - }, - { - code: 6026; - name: 'MintHasNoFreezeAuthority'; - }, - { - code: 6027; - name: 'MintWithInvalidExtension'; - }, - { - code: 6028; - name: 'InsufficientTokenAccountBalance'; - msg: 'The token account balance is less than the remaining amount.'; + name: 'SerializationError'; + msg: 'Serialization error'; }, ]; }; @@ -1715,6 +1648,57 @@ export const IDL: LightCompressedToken = { ], args: [], }, + { + name: 'addTokenPool', + docs: [ + 'This instruction creates an additional token pool for a given mint.', + 'The maximum number of token pools per mint is 5.', + ], + accounts: [ + { + name: 'feePayer', + isMut: true, + isSigner: true, + docs: ['UNCHECKED: only pays fees.'], + }, + { + name: 'tokenPoolPda', + isMut: true, + isSigner: false, + }, + { + name: 'existingTokenPoolPda', + isMut: false, + isSigner: false, + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false, + }, + { + name: 'mint', + isMut: true, + isSigner: false, + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false, + }, + { + name: 'cpiAuthorityPda', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'tokenPoolBump', + type: 'u8', + }, + ], + }, { name: 'mintTo', docs: [ @@ -3205,141 +3189,23 @@ export const IDL: LightCompressedToken = { errors: [ { code: 6000, - name: 'PublicKeyAmountMissmatch', - msg: 'public keys and amounts must be of same length', + name: 'SignerCheckFailed', + msg: 'Signer check failed', }, { code: 6001, - name: 'ComputeInputSumFailed', - msg: 'ComputeInputSumFailed', + name: 'CreateTransferInstructionFailed', + msg: 'Create transfer instruction failed', }, { code: 6002, - name: 'ComputeOutputSumFailed', - msg: 'ComputeOutputSumFailed', + name: 'AccountNotFound', + msg: 'Account not found', }, { code: 6003, - name: 'ComputeCompressSumFailed', - msg: 'ComputeCompressSumFailed', - }, - { - code: 6004, - name: 'ComputeDecompressSumFailed', - msg: 'ComputeDecompressSumFailed', - }, - { - code: 6005, - name: 'SumCheckFailed', - msg: 'SumCheckFailed', - }, - { - code: 6006, - name: 'DecompressRecipientUndefinedForDecompress', - msg: 'DecompressRecipientUndefinedForDecompress', - }, - { - code: 6007, - name: 'CompressedPdaUndefinedForDecompress', - msg: 'CompressedPdaUndefinedForDecompress', - }, - { - code: 6008, - name: 'DeCompressAmountUndefinedForDecompress', - msg: 'DeCompressAmountUndefinedForDecompress', - }, - { - code: 6009, - name: 'CompressedPdaUndefinedForCompress', - msg: 'CompressedPdaUndefinedForCompress', - }, - { - code: 6010, - name: 'DeCompressAmountUndefinedForCompress', - msg: 'DeCompressAmountUndefinedForCompress', - }, - { - code: 6011, - name: 'DelegateSignerCheckFailed', - msg: 'DelegateSignerCheckFailed', - }, - { - code: 6012, - name: 'MintTooLarge', - msg: 'Minted amount greater than u64::MAX', - }, - { - code: 6013, - name: 'SplTokenSupplyMismatch', - msg: 'SplTokenSupplyMismatch', - }, - { - code: 6014, - name: 'HeapMemoryCheckFailed', - msg: 'HeapMemoryCheckFailed', - }, - { - code: 6015, - name: 'InstructionNotCallable', - msg: 'The instruction is not callable', - }, - { - code: 6016, - name: 'ArithmeticUnderflow', - msg: 'ArithmeticUnderflow', - }, - { - code: 6017, - name: 'HashToFieldError', - msg: 'HashToFieldError', - }, - { - code: 6018, - name: 'InvalidAuthorityMint', - msg: 'Expected the authority to be also a mint authority', - }, - { - code: 6019, - name: 'InvalidFreezeAuthority', - msg: 'Provided authority is not the freeze authority', - }, - { - code: 6020, - name: 'InvalidDelegateIndex', - }, - { - code: 6021, - name: 'TokenPoolPdaUndefined', - }, - { - code: 6022, - name: 'IsTokenPoolPda', - msg: 'Compress or decompress recipient is the same account as the token pool pda.', - }, - { - code: 6023, - name: 'InvalidTokenPoolPda', - }, - { - code: 6024, - name: 'NoInputTokenAccountsProvided', - }, - { - code: 6025, - name: 'NoInputsProvided', - }, - { - code: 6026, - name: 'MintHasNoFreezeAuthority', - }, - { - code: 6027, - name: 'MintWithInvalidExtension', - }, - { - code: 6028, - name: 'InsufficientTokenAccountBalance', - msg: 'The token account balance is less than the remaining amount.', + name: 'SerializationError', + msg: 'Serialization error', }, ], }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c69e74ce7..6c0c60d79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -398,6 +398,10 @@ importers: specifier: ^1.6.0 version: 1.6.0(@types/node@22.10.2)(@vitest/browser@1.6.0)(terser@5.31.0) + hasher.rs/src/main/wasm: {} + + hasher.rs/src/main/wasm-simd: {} + js/compressed-token: dependencies: '@coral-xyz/anchor': diff --git a/programs/compressed-token/src/burn.rs b/programs/compressed-token/src/burn.rs index 5352629de..b2233ad52 100644 --- a/programs/compressed-token/src/burn.rs +++ b/programs/compressed-token/src/burn.rs @@ -15,6 +15,7 @@ use crate::{ get_input_compressed_accounts_with_merkle_context_and_check_signer, DelegatedTransfer, InputTokenDataWithContext, }, + spl_compression::invoke_token_program_with_multiple_token_pool_accounts, BurnInstruction, ErrorCode, }; @@ -68,32 +69,46 @@ pub fn burn_spl_from_pool_pda<'info>( ctx: &Context<'_, '_, '_, 'info, BurnInstruction<'info>>, inputs: &CompressedTokenInstructionDataBurn, ) -> Result<()> { - let pre_token_balance = ctx.accounts.token_pool_pda.amount; + let amount = inputs.burn_amount; + let token_pool_pda = &ctx.accounts.token_pool_pda; + + invoke_token_program_with_multiple_token_pool_accounts::( + ctx.remaining_accounts, + &ctx.accounts.mint.key().to_bytes(), + Some(ctx.accounts.mint.to_account_info()), + None, + ctx.accounts.cpi_authority_pda.to_account_info(), + ctx.accounts.token_program.to_account_info(), + token_pool_pda.to_account_info(), + amount, + ) +} + +pub fn spl_burn_cpi<'info>( + mint: AccountInfo<'info>, + cpi_authority_pda: AccountInfo<'info>, + token_pool_pda: AccountInfo<'info>, + token_program: AccountInfo<'info>, + burn_amount: u64, + pre_token_balance: u64, +) -> Result<()> { let cpi_accounts = anchor_spl::token_interface::Burn { - mint: ctx.accounts.mint.to_account_info(), - from: ctx.accounts.token_pool_pda.to_account_info(), - authority: ctx.accounts.cpi_authority_pda.to_account_info(), + mint, + from: token_pool_pda.to_account_info(), + authority: cpi_authority_pda, }; let signer_seeds = get_cpi_signer_seeds(); let signer_seeds_ref = &[&signer_seeds[..]]; - let cpi_ctx = CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - cpi_accounts, - signer_seeds_ref, - ); - anchor_spl::token_interface::burn(cpi_ctx, inputs.burn_amount)?; - - let post_token_balance = TokenAccount::try_deserialize( - &mut &ctx.accounts.token_pool_pda.to_account_info().data.borrow()[..], - )? - .amount; - // Guard against unexpected behavior of the SPL token program. - if post_token_balance != pre_token_balance - inputs.burn_amount { + let cpi_ctx = CpiContext::new_with_signer(token_program, cpi_accounts, signer_seeds_ref); + anchor_spl::token_interface::burn(cpi_ctx, burn_amount)?; + let post_token_balance = + TokenAccount::try_deserialize(&mut &token_pool_pda.data.borrow()[..])?.amount; + if post_token_balance != pre_token_balance - burn_amount { msg!( "post_token_balance {} != pre_token_balance {} - burn_amount {}", post_token_balance, pre_token_balance, - inputs.burn_amount + burn_amount ); return err!(crate::ErrorCode::SplTokenSupplyMismatch); } @@ -182,7 +197,7 @@ pub mod sdk { use super::CompressedTokenInstructionDataBurn; use crate::{ - get_token_pool_pda, + get_token_pool_pda_with_bump, process_transfer::{ get_cpi_authority_pda, transfer_sdk::{ @@ -206,6 +221,8 @@ pub mod sdk { pub burn_amount: u64, pub signer_is_delegate: bool, pub is_token_22: bool, + pub token_pool_bump: u8, + pub additonal_pool_accounts: Vec, } pub fn create_burn_instruction( @@ -213,7 +230,11 @@ pub mod sdk { ) -> Result { let (remaining_accounts, input_token_data_with_context, _) = create_input_output_and_remaining_accounts( - &[inputs.change_account_merkle_tree], + &[ + inputs.additonal_pool_accounts, + vec![inputs.change_account_merkle_tree], + ] + .concat(), &inputs.input_token_data, &inputs.input_compressed_accounts, &inputs.input_merkle_contexts, @@ -253,7 +274,7 @@ pub mod sdk { } .data(); - let token_pool_pda = get_token_pool_pda(&inputs.mint); + let token_pool_pda = get_token_pool_pda_with_bump(&inputs.mint, inputs.token_pool_bump); let token_program = if inputs.is_token_22 { anchor_spl::token_2022::ID } else { diff --git a/programs/compressed-token/src/constants.rs b/programs/compressed-token/src/constants.rs index 10943b488..67b9ab70f 100644 --- a/programs/compressed-token/src/constants.rs +++ b/programs/compressed-token/src/constants.rs @@ -2,3 +2,7 @@ pub const TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR: [u8; 8] = [2, 0, 0, 0, 0, 0, 0, 0]; pub const BUMP_CPI_AUTHORITY: u8 = 254; pub const NOT_FROZEN: bool = false; +pub const POOL_SEED: &[u8] = b"pool"; + +/// Maximum number of pool accounts that can be created for each mint. +pub const NUM_MAX_POOL_ACCOUNTS: u8 = 5; diff --git a/programs/compressed-token/src/instructions/burn.rs b/programs/compressed-token/src/instructions/burn.rs index c670567b9..d2835ba5b 100644 --- a/programs/compressed-token/src/instructions/burn.rs +++ b/programs/compressed-token/src/instructions/burn.rs @@ -1,12 +1,12 @@ use account_compression::{program::AccountCompression, utils::constants::CPI_AUTHORITY_PDA_SEED}; use anchor_lang::prelude::*; -use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::token_interface::{Mint, TokenInterface}; use light_system_program::{ program::LightSystemProgram, sdk::accounts::{InvokeAccounts, SignerAccounts}, }; -use crate::{program::LightCompressedToken, POOL_SEED}; +use crate::program::LightCompressedToken; #[derive(Accounts)] pub struct BurnInstruction<'info> { @@ -24,9 +24,9 @@ pub struct BurnInstruction<'info> { /// CHECK: is used to burn tokens. #[account(mut)] pub mint: InterfaceAccount<'info, Mint>, - /// CHECK: (seed constraint) is derived from mint account. - #[account(mut, seeds = [POOL_SEED, mint.key().as_ref()], bump)] - pub token_pool_pda: InterfaceAccount<'info, TokenAccount>, + /// CHECK: in burn_spl_from_pool_pda. + #[account(mut)] + pub token_pool_pda: AccountInfo<'info>, pub token_program: Interface<'info, TokenInterface>, pub light_system_program: Program<'info, LightSystemProgram>, /// CHECK: (account compression program). diff --git a/programs/compressed-token/src/instructions/create_token_pool.rs b/programs/compressed-token/src/instructions/create_token_pool.rs index 6f93c5a73..caa671ac1 100644 --- a/programs/compressed-token/src/instructions/create_token_pool.rs +++ b/programs/compressed-token/src/instructions/create_token_pool.rs @@ -6,7 +6,10 @@ use spl_token_2022::{ pod::PodMint, }; -pub const POOL_SEED: &[u8] = b"pool"; +use crate::{ + constants::{NUM_MAX_POOL_ACCOUNTS, POOL_SEED}, + spl_compression::check_spl_token_pool_derivation, +}; /// Creates an SPL or token-2022 token pool account, which is owned by the token authority PDA. #[derive(Accounts)] @@ -36,7 +39,16 @@ pub struct CreateTokenPoolInstruction<'info> { } pub fn get_token_pool_pda(mint: &Pubkey) -> Pubkey { - let seeds = &[POOL_SEED, mint.as_ref()]; + get_token_pool_pda_with_bump(mint, 0) +} + +pub fn get_token_pool_pda_with_bump(mint: &Pubkey, token_pool_bump: u8) -> Pubkey { + let seeds = &[POOL_SEED, mint.as_ref(), &[token_pool_bump]]; + let seeds = if token_pool_bump == 0 { + &seeds[..2] + } else { + &seeds[..] + }; let (address, _) = Pubkey::find_program_address(seeds, &crate::ID); address } @@ -62,3 +74,81 @@ pub fn assert_mint_extensions(account_data: &[u8]) -> Result<()> { } Ok(()) } + +/// Creates an SPL or token-2022 token pool account, which is owned by the token authority PDA. +#[derive(Accounts)] +#[instruction(token_pool_bump: u8)] +pub struct AddTokenPoolInstruction<'info> { + /// UNCHECKED: only pays fees. + #[account(mut)] + pub fee_payer: Signer<'info>, + #[account( + init, + seeds = [ + POOL_SEED, &mint.key().to_bytes(), &[token_pool_bump], + ], + bump, + payer = fee_payer, + token::mint = mint, + token::authority = cpi_authority_pda, + )] + pub token_pool_pda: InterfaceAccount<'info, TokenAccount>, + pub existing_token_pool_pda: InterfaceAccount<'info, TokenAccount>, + pub system_program: Program<'info, System>, + /// CHECK: is mint account. + #[account(mut)] + pub mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, + /// CHECK: (seeds anchor constraint). + #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] + pub cpi_authority_pda: AccountInfo<'info>, +} + +/// Checks if the token pool PDA is valid. +/// Iterates over all possible bump seeds to check if the token pool PDA is valid. +#[inline(always)] +pub fn is_valid_token_pool_pda(token_pool_pda: &Pubkey, mint: &Pubkey) -> Result<()> { + let mint_bytes = mint.to_bytes(); + let is_valid_token_pool_pda = (0..NUM_MAX_POOL_ACCOUNTS) + .any(|i| check_spl_token_pool_derivation(mint_bytes.as_slice(), token_pool_pda, &[i])); + if !is_valid_token_pool_pda { + err!(crate::ErrorCode::InvalidTokenPoolPda) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + /// Test: + /// 1. Functional: test_is_valid_token_pool_pda_valid + /// 2. Failing: test_is_valid_token_pool_pda_invalid_derivation + /// 3. Failing: test_is_valid_token_pool_pda_bump_seed_equal_to_num_max_accounts + /// 4. Failing: test_is_valid_token_pool_pda_bump_seed_larger_than_num_max_accounts + #[test] + fn test_is_valid_token_pool_pda() { + // 1. Functional: test_is_valid_token_pool_pda_valid + let mint = Pubkey::new_unique(); + for i in 0..NUM_MAX_POOL_ACCOUNTS { + let valid_pda = get_token_pool_pda_with_bump(&mint, i); + assert!(is_valid_token_pool_pda(&valid_pda, &mint).is_ok()); + } + + // 2. Failing: test_is_valid_token_pool_pda_invalid_derivation + let mint = Pubkey::new_unique(); + let invalid_pda = Pubkey::new_unique(); + assert!(is_valid_token_pool_pda(&invalid_pda, &mint).is_err()); + + // 3. Failing: test_is_valid_token_pool_pda_bump_seed_equal_to_num_max_accounts + let mint = Pubkey::new_unique(); + let invalid_pda = get_token_pool_pda_with_bump(&mint, NUM_MAX_POOL_ACCOUNTS); + assert!(is_valid_token_pool_pda(&invalid_pda, &mint).is_err()); + + // 4. Failing: test_is_valid_token_pool_pda_bump_seed_larger_than_num_max_accounts + let mint = Pubkey::new_unique(); + let invalid_pda = get_token_pool_pda_with_bump(&mint, NUM_MAX_POOL_ACCOUNTS + 1); + assert!(is_valid_token_pool_pda(&invalid_pda, &mint).is_err()); + } +} diff --git a/programs/compressed-token/src/lib.rs b/programs/compressed-token/src/lib.rs index 692bf37bc..eee49faf6 100644 --- a/programs/compressed-token/src/lib.rs +++ b/programs/compressed-token/src/lib.rs @@ -32,7 +32,8 @@ solana_security_txt::security_txt! { #[program] pub mod light_compressed_token { - use constants::NOT_FROZEN; + use constants::{NOT_FROZEN, NUM_MAX_POOL_ACCOUNTS}; + use spl_compression::spl_token_pool_derivation; use super::*; @@ -48,6 +49,23 @@ pub mod light_compressed_token { ) } + /// This instruction creates an additional token pool for a given mint. + /// The maximum number of token pools per mint is 5. + pub fn add_token_pool<'info>( + ctx: Context<'_, '_, '_, 'info, AddTokenPoolInstruction<'info>>, + token_pool_bump: u8, + ) -> Result<()> { + if token_pool_bump >= NUM_MAX_POOL_ACCOUNTS { + return err!(ErrorCode::InvalidTokenPoolBump); + } + // Check that token pool account with previous bump already exists. + spl_token_pool_derivation( + &ctx.accounts.mint.key().to_bytes(), + &ctx.accounts.existing_token_pool_pda.key(), + &[token_pool_bump.saturating_sub(1)], + ) + } + /// Mints tokens from an spl token mint to a list of compressed accounts. /// Minted tokens are transferred to a pool account owned by the compressed /// token program. The instruction creates one compressed output account for @@ -211,4 +229,8 @@ pub enum ErrorCode { MintWithInvalidExtension, #[msg("The token account balance is less than the remaining amount.")] InsufficientTokenAccountBalance, + #[msg("Max number of token pools reached.")] + InvalidTokenPoolBump, + FailedToDecompress, + FailedToBurnSplTokensFromTokenPool, } diff --git a/programs/compressed-token/src/process_compress_spl_token_account.rs b/programs/compressed-token/src/process_compress_spl_token_account.rs index 3e652d7be..5c6df666d 100644 --- a/programs/compressed-token/src/process_compress_spl_token_account.rs +++ b/programs/compressed-token/src/process_compress_spl_token_account.rs @@ -54,7 +54,7 @@ pub mod sdk { use light_system_program::sdk::CompressedCpiContext; use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; - use crate::get_token_pool_pda; + use crate::get_token_pool_pda_with_bump; #[allow(clippy::too_many_arguments)] pub fn create_compress_spl_token_account_instruction( @@ -67,6 +67,7 @@ pub mod sdk { output_merkle_tree: &Pubkey, token_account: &Pubkey, is_token_22: bool, + token_pool_bump: u8, ) -> Instruction { let instruction_data = crate::instruction::CompressSplTokenAccount { owner: *owner, @@ -74,7 +75,7 @@ pub mod sdk { cpi_context, }; let (cpi_authority_pda, _) = crate::process_transfer::get_cpi_authority_pda(); - let token_pool_pda = get_token_pool_pda(mint); + let token_pool_pda = get_token_pool_pda_with_bump(mint, token_pool_bump); let token_program = if is_token_22 { Some(anchor_spl::token_2022::ID) } else { diff --git a/programs/compressed-token/src/process_mint.rs b/programs/compressed-token/src/process_mint.rs index ba512d810..faf6fbf4a 100644 --- a/programs/compressed-token/src/process_mint.rs +++ b/programs/compressed-token/src/process_mint.rs @@ -10,7 +10,7 @@ use { light_utils::hash_to_bn254_field_size_be, }; -use crate::{program::LightCompressedToken, POOL_SEED}; +use crate::{is_valid_token_pool_pda, program::LightCompressedToken}; /// Mints tokens from an spl token mint to a list of compressed accounts and /// stores minted tokens in spl token pool account. @@ -274,6 +274,7 @@ pub fn serialize_mint_to_cpi_instruction_data( #[inline(never)] pub fn mint_spl_to_pool_pda(ctx: &Context, amounts: &[u64]) -> Result<()> { + is_valid_token_pool_pda(&ctx.accounts.token_pool_pda.key(), &ctx.accounts.mint.key())?; let mut mint_amount: u64 = 0; for amount in amounts.iter() { mint_amount = mint_amount @@ -324,7 +325,8 @@ pub struct MintToInstruction<'info> { @ crate::ErrorCode::InvalidAuthorityMint )] pub mint: InterfaceAccount<'info, Mint>, - #[account(mut, seeds = [POOL_SEED, mint.key().as_ref()], bump)] + /// CHECK: with is_valid_token_pool_pda(). + #[account(mut)] pub token_pool_pda: InterfaceAccount<'info, TokenAccount>, pub token_program: Interface<'info, TokenInterface>, pub light_system_program: Program<'info, LightSystemProgram>, @@ -355,7 +357,9 @@ pub mod mint_sdk { use light_system_program::sdk::invoke::get_sol_pool_pda; use solana_sdk::{instruction::Instruction, pubkey::Pubkey}; - use crate::{get_token_pool_pda, process_transfer::get_cpi_authority_pda}; + use crate::{ + get_token_pool_pda, get_token_pool_pda_with_bump, process_transfer::get_cpi_authority_pda, + }; pub fn create_create_token_pool_instruction( fee_payer: &Pubkey, @@ -386,6 +390,39 @@ pub mod mint_sdk { } } + pub fn create_add_token_pool_instruction( + fee_payer: &Pubkey, + mint: &Pubkey, + token_pool_bump: u8, + is_token_22: bool, + ) -> Instruction { + let token_pool_pda = get_token_pool_pda_with_bump(mint, token_pool_bump); + let existing_token_pool_pda = + get_token_pool_pda_with_bump(mint, token_pool_bump.saturating_sub(1)); + let instruction_data = crate::instruction::AddTokenPool { token_pool_bump }; + + let token_program: Pubkey = if is_token_22 { + anchor_spl::token_2022::ID + } else { + anchor_spl::token::ID + }; + let accounts = crate::accounts::AddTokenPoolInstruction { + fee_payer: *fee_payer, + token_pool_pda, + system_program: system_program::ID, + mint: *mint, + token_program, + cpi_authority_pda: get_cpi_authority_pda().0, + existing_token_pool_pda, + }; + + Instruction { + program_id: crate::ID, + accounts: accounts.to_account_metas(Some(true)), + data: instruction_data.data(), + } + } + #[allow(clippy::too_many_arguments)] pub fn create_mint_to_instruction( fee_payer: &Pubkey, @@ -396,8 +433,9 @@ pub mod mint_sdk { public_keys: Vec, lamports: Option, token_2022: bool, + token_pool_bump: u8, ) -> Instruction { - let token_pool_pda = get_token_pool_pda(mint); + let token_pool_pda = get_token_pool_pda_with_bump(mint, token_pool_bump); let instruction_data = crate::instruction::MintTo { amounts, @@ -448,6 +486,7 @@ pub mod mint_sdk { } #[cfg(test)] + mod test { use light_hasher::Poseidon; use light_system_program::{ @@ -460,6 +499,7 @@ mod test { constants::TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR, token_data::{AccountState, TokenData}, }; + #[test] fn test_manual_ix_data_serialization_borsh_compat() { let pubkeys = vec![Pubkey::new_unique(), Pubkey::new_unique()]; diff --git a/programs/compressed-token/src/process_transfer.rs b/programs/compressed-token/src/process_transfer.rs index fcb233331..ad478d049 100644 --- a/programs/compressed-token/src/process_transfer.rs +++ b/programs/compressed-token/src/process_transfer.rs @@ -643,6 +643,7 @@ pub mod transfer_sdk { delegate_change_account_index: Option, lamports_change_account_merkle_tree: Option, is_token_22: bool, + additional_token_pools: &[Pubkey], ) -> Result { let (remaining_accounts, mut inputs_struct) = create_inputs_and_remaining_accounts( input_token_data, @@ -657,6 +658,7 @@ pub mod transfer_sdk { compress_or_decompress_amount, delegate_change_account_index, lamports_change_account_merkle_tree, + additional_token_pools, ); if sort { inputs_struct @@ -759,6 +761,7 @@ pub mod transfer_sdk { compress_or_decompress_amount, delegate_change_account_index, lamports_change_account_merkle_tree, + &[], ); Ok((remaining_accounts, compressed_accounts_ix_data)) } @@ -777,11 +780,13 @@ pub mod transfer_sdk { compress_or_decompress_amount: Option, delegate_change_account_index: Option, lamports_change_account_merkle_tree: Option, + accounts: &[Pubkey], ) -> ( HashMap, CompressedTokenInstructionDataTransfer, ) { let mut additonal_accounts = Vec::new(); + additonal_accounts.extend_from_slice(accounts); if let Some(delegate) = delegate { additonal_accounts.push(delegate); for account in input_token_data.iter() { diff --git a/programs/compressed-token/src/spl_compression.rs b/programs/compressed-token/src/spl_compression.rs index 85d4525c8..317734201 100644 --- a/programs/compressed-token/src/spl_compression.rs +++ b/programs/compressed-token/src/spl_compression.rs @@ -1,15 +1,16 @@ #![allow(deprecated)] use anchor_lang::{prelude::*, solana_program::account_info::AccountInfo}; -use anchor_spl::token_interface; +use anchor_spl::{token::TokenAccount, token_interface}; use crate::{ - process_transfer::get_cpi_signer_seeds, CompressedTokenInstructionDataTransfer, - TransferInstruction, POOL_SEED, + constants::{NUM_MAX_POOL_ACCOUNTS, POOL_SEED}, + process_transfer::get_cpi_signer_seeds, + CompressedTokenInstructionDataTransfer, TransferInstruction, }; -pub fn process_compression_or_decompression( +pub fn process_compression_or_decompression<'info>( inputs: &CompressedTokenInstructionDataTransfer, - ctx: &Context, + ctx: &Context<'_, '_, '_, 'info, TransferInstruction<'info>>, ) -> Result<()> { if inputs.is_compress { compress_spl_tokens(inputs, ctx) @@ -19,22 +20,35 @@ pub fn process_compression_or_decompression( } pub fn spl_token_pool_derivation( - mint: &Pubkey, - program_id: &Pubkey, + mint_bytes: &[u8], token_pool_pubkey: &Pubkey, + bump: &[u8], ) -> Result<()> { - let seeds = &[POOL_SEED, &mint.to_bytes()[..]]; - let (pda, _bump_seed) = Pubkey::find_program_address(seeds, program_id); - if pda == *token_pool_pubkey { + if check_spl_token_pool_derivation(mint_bytes, token_pool_pubkey, bump) { Ok(()) } else { err!(crate::ErrorCode::InvalidTokenPoolPda) } } -pub fn decompress_spl_tokens( +pub fn check_spl_token_pool_derivation( + mint_bytes: &[u8], + token_pool_pubkey: &Pubkey, + bump: &[u8], +) -> bool { + let seeds = [POOL_SEED, mint_bytes, bump]; + let seeds = if bump[0] == 0 { + &seeds[..2] + } else { + &seeds[..] + }; + let (pda, _) = Pubkey::find_program_address(seeds, &crate::ID); + pda == *token_pool_pubkey +} + +pub fn decompress_spl_tokens<'info>( inputs: &CompressedTokenInstructionDataTransfer, - ctx: &Context, + ctx: &Context<'_, '_, '_, 'info, TransferInstruction<'info>>, ) -> Result<()> { let recipient = match ctx.accounts.compress_or_decompress_token_account.as_ref() { Some(compression_recipient) => compression_recipient.to_account_info(), @@ -44,58 +58,139 @@ pub fn decompress_spl_tokens( Some(token_pool_pda) => token_pool_pda.to_account_info(), None => return err!(crate::ErrorCode::CompressedPdaUndefinedForDecompress), }; - spl_token_pool_derivation(&inputs.mint, &crate::ID, &token_pool_pda.key())?; - let amount = match inputs.compress_or_decompress_amount { Some(amount) => amount, None => return err!(crate::ErrorCode::DeCompressAmountUndefinedForDecompress), }; - - transfer( - token_pool_pda, - recipient, + invoke_token_program_with_multiple_token_pool_accounts::( + ctx.remaining_accounts, + &inputs.mint.key().to_bytes(), + None, + Some(recipient), ctx.accounts.cpi_authority_pda.to_account_info(), ctx.accounts .token_program .as_ref() .unwrap() .to_account_info(), + token_pool_pda, amount, ) } -pub fn compress_spl_tokens( +/// Executes a token program instruction with multiple token pool accounts. +/// Supported instructions are burn and transfer to decompress spl tokens. +#[allow(clippy::too_many_arguments)] +pub fn invoke_token_program_with_multiple_token_pool_accounts<'info, const IS_BURN: bool>( + remaining_accounts: &[AccountInfo<'info>], + mint_bytes: &[u8; 32], + mint: Option>, + recipient: Option>, + cpi_authority_pda: AccountInfo<'info>, + token_program: AccountInfo<'info>, + mut token_pool_pda: AccountInfo<'info>, + mut amount: u64, +) -> std::result::Result<(), Error> { + let mut token_pool_bumps = (0..NUM_MAX_POOL_ACCOUNTS).collect::>(); + + for i in 0..NUM_MAX_POOL_ACCOUNTS { + if i != 0 { + token_pool_pda = remaining_accounts[i as usize - 1].to_account_info(); + } + let token_pool_amount = + TokenAccount::try_deserialize(&mut &token_pool_pda.data.borrow()[..]) + .map_err(|_| crate::ErrorCode::InvalidTokenPoolPda)? + .amount; + let action_amount = std::cmp::min(amount, token_pool_amount); + if action_amount == 0 { + continue; + } + let mut remove_index = 0; + for (index, i) in token_pool_bumps.iter().enumerate() { + if check_spl_token_pool_derivation(mint_bytes.as_slice(), &token_pool_pda.key(), &[*i]) + { + if IS_BURN { + crate::burn::spl_burn_cpi( + mint.clone().unwrap(), + cpi_authority_pda.to_account_info(), + token_pool_pda.to_account_info(), + token_program.to_account_info(), + action_amount, + token_pool_amount, + )?; + } else { + crate::spl_compression::spl_token_transfer_cpi_with_signer( + token_pool_pda.to_account_info(), + recipient.clone().unwrap(), + cpi_authority_pda.to_account_info(), + token_program.to_account_info(), + action_amount, + )?; + } + + remove_index = index; + } + } + token_pool_bumps.remove(remove_index); + + amount = amount.saturating_sub(action_amount); + if amount == 0 { + return Ok(()); + } + } + + msg!("Remaining amount: {}.", amount); + if IS_BURN { + msg!("Token pool account balances insufficient for burn. \nTry to pass more token pool accounts."); + err!(crate::ErrorCode::FailedToBurnSplTokensFromTokenPool) + } else { + msg!("Token pool account balances insufficient for decompression. \nTry to pass more token pool accounts."); + err!(crate::ErrorCode::FailedToDecompress) + } +} + +pub fn compress_spl_tokens<'info>( inputs: &CompressedTokenInstructionDataTransfer, - ctx: &Context, + ctx: &Context<'_, '_, '_, 'info, TransferInstruction<'info>>, ) -> Result<()> { let recipient_token_pool = match ctx.accounts.token_pool_pda.as_ref() { - Some(token_pool_pda) => token_pool_pda, + Some(token_pool_pda) => token_pool_pda.to_account_info(), None => return err!(crate::ErrorCode::CompressedPdaUndefinedForCompress), }; - spl_token_pool_derivation(&inputs.mint, &crate::ID, &recipient_token_pool.key())?; let amount = match inputs.compress_or_decompress_amount { Some(amount) => amount, None => return err!(crate::ErrorCode::DeCompressAmountUndefinedForCompress), }; - transfer_compress( - ctx.accounts - .compress_or_decompress_token_account - .as_ref() - .unwrap() - .to_account_info(), - recipient_token_pool.to_account_info(), - ctx.accounts.authority.to_account_info(), - ctx.accounts - .token_program - .as_ref() - .unwrap() - .to_account_info(), - amount, - ) + let mint_bytes = inputs.mint.to_bytes(); + + for i in 0..NUM_MAX_POOL_ACCOUNTS { + if check_spl_token_pool_derivation(mint_bytes.as_slice(), &recipient_token_pool.key(), &[i]) + { + spl_token_transfer( + ctx.accounts + .compress_or_decompress_token_account + .as_ref() + .unwrap() + .to_account_info(), + recipient_token_pool.to_account_info(), + ctx.accounts.authority.to_account_info(), + ctx.accounts + .token_program + .as_ref() + .unwrap() + .to_account_info(), + amount, + )?; + return Ok(()); + } + } + err!(crate::ErrorCode::InvalidTokenPoolPda) } -pub fn transfer<'info>( +/// Invoke the spl token burn instruction with cpi authority pda as signer. +/// Used to decompress spl tokens. +pub fn spl_token_transfer_cpi_with_signer<'info>( from: AccountInfo<'info>, to: AccountInfo<'info>, authority: AccountInfo<'info>, @@ -114,7 +209,9 @@ pub fn transfer<'info>( anchor_spl::token_interface::transfer(cpi_ctx, amount) } -pub fn transfer_compress<'info>( +/// Invoke the spl token transfer instruction with transaction signer. +/// Used to compress spl tokens. +pub fn spl_token_transfer<'info>( from: AccountInfo<'info>, to: AccountInfo<'info>, authority: AccountInfo<'info>, diff --git a/test-programs/compressed-token-test/tests/test.rs b/test-programs/compressed-token-test/tests/test.rs index 2ad3dfc24..2d948dad3 100644 --- a/test-programs/compressed-token-test/tests/test.rs +++ b/test-programs/compressed-token-test/tests/test.rs @@ -2,19 +2,21 @@ use account_compression::errors::AccountCompressionErrorCode; use anchor_lang::{ - system_program, AnchorDeserialize, AnchorSerialize, InstructionData, ToAccountMetas, + prelude::AccountMeta, system_program, AnchorDeserialize, AnchorSerialize, InstructionData, + ToAccountMetas, }; use anchor_spl::{ token::{Mint, TokenAccount}, token_2022::{spl_token_2022, spl_token_2022::extension::ExtensionType}, }; use light_compressed_token::{ + constants::NUM_MAX_POOL_ACCOUNTS, delegation::sdk::{ create_approve_instruction, create_revoke_instruction, CreateApproveInstructionInputs, CreateRevokeInstructionInputs, }, freeze::sdk::{create_instruction, CreateInstructionInputs}, - get_token_pool_pda, + get_token_pool_pda, get_token_pool_pda_with_bump, mint_sdk::{create_create_token_pool_instruction, create_mint_to_instruction}, process_transfer::{ get_cpi_authority_pda, transfer_sdk::create_transfer_instruction, TokenTransferOutputData, @@ -37,21 +39,22 @@ use light_test_utils::{ indexer::TestIndexer, spl::{ approve_test, burn_test, compress_test, compressed_transfer_22_test, - compressed_transfer_test, create_burn_test_instruction, create_mint_22_helper, - create_mint_helper, create_token_2022_account, decompress_test, freeze_test, - mint_spl_tokens, mint_tokens_22_helper_with_lamports, mint_tokens_helper, + compressed_transfer_test, create_additional_token_pools, create_burn_test_instruction, + create_mint_22_helper, create_mint_helper, create_token_2022_account, decompress_test, + freeze_test, mint_spl_tokens, mint_tokens_22_helper_with_lamports, + mint_tokens_22_helper_with_lamports_and_bump, mint_tokens_helper, mint_tokens_helper_with_lamports, mint_wrapped_sol, perform_compress_spl_token_account, revoke_test, thaw_test, BurnInstructionMode, }, Indexer, RpcConnection, RpcError, TokenDataWithContext, }; use light_verifier::VerifierError; -use rand::Rng; +use rand::{seq::SliceRandom, thread_rng, Rng}; use serial_test::serial; use solana_sdk::{ instruction::{Instruction, InstructionError}, pubkey::Pubkey, - signature::Keypair, + signature::{Keypair, Signature}, signer::Signer, system_instruction, transaction::{Transaction, TransactionError}, @@ -63,7 +66,14 @@ use spl_token::{error::TokenError, instruction::initialize_mint}; async fn test_create_mint() { let (mut rpc, _) = setup_test_programs_with_accounts(None).await; let payer = rpc.get_payer().insecure_clone(); - create_mint_helper(&mut rpc, &payer).await; + let mint = create_mint_helper(&mut rpc, &payer).await; + create_additional_token_pools(&mut rpc, &payer, &mint, false, NUM_MAX_POOL_ACCOUNTS) + .await + .unwrap(); + let mint_22 = create_mint_22_helper(&mut rpc, &payer).await; + create_additional_token_pools(&mut rpc, &payer, &mint_22, true, NUM_MAX_POOL_ACCOUNTS) + .await + .unwrap(); } #[serial] @@ -311,16 +321,257 @@ async fn test_failing_create_token_pool() { let token_pool_pubkey = get_token_pool_pda(&mint.pubkey()); let token_pool_account = rpc.get_account(token_pool_pubkey).await.unwrap().unwrap(); - spl_token_pool_derivation( - &mint.pubkey(), - &light_compressed_token::ID, - &token_pool_pubkey, - ) - .unwrap(); + spl_token_pool_derivation(&mint.pubkey().to_bytes(), &token_pool_pubkey, &[0]).unwrap(); assert_eq!(token_pool_account.data.len(), TokenAccount::LEN); } } +#[serial] +#[tokio::test] +async fn failing_tests_add_token_pool() { + for is_token_22 in vec![false, true] { + let (mut rpc, _) = setup_test_programs_with_accounts(None).await; + let payer = rpc.get_payer().insecure_clone(); + + let mint = if !is_token_22 { + create_mint_helper(&mut rpc, &payer).await + } else { + create_mint_22_helper(&mut rpc, &payer).await + }; + let invalid_mint = if !is_token_22 { + create_mint_helper(&mut rpc, &payer).await + } else { + create_mint_22_helper(&mut rpc, &payer).await + }; + let mut current_token_pool_bump = 1; + create_additional_token_pools(&mut rpc, &payer, &mint, is_token_22, 2) + .await + .unwrap(); + create_additional_token_pools(&mut rpc, &payer, &invalid_mint, is_token_22, 2) + .await + .unwrap(); + current_token_pool_bump += 2; + // 1. failing invalid existing token pool pda + { + let result = add_token_pool( + &mut rpc, + &payer, + &mint, + None, + current_token_pool_bump, + is_token_22, + FailingTestsAddTokenPool::InvalidExistingTokenPoolPda, + ) + .await; + assert_rpc_error(result, 0, ErrorCode::InvalidTokenPoolPda.into()).unwrap(); + } + // 2. failing InvalidTokenPoolPda + { + let result = add_token_pool( + &mut rpc, + &payer, + &mint, + None, + current_token_pool_bump, + is_token_22, + FailingTestsAddTokenPool::InvalidTokenPoolPda, + ) + .await; + assert_rpc_error( + result, + 0, + anchor_lang::error::ErrorCode::ConstraintSeeds.into(), + ) + .unwrap(); + } + // 3. failing invalid system program id + { + let result = add_token_pool( + &mut rpc, + &payer, + &mint, + None, + current_token_pool_bump, + is_token_22, + FailingTestsAddTokenPool::InvalidSystemProgramId, + ) + .await; + assert_rpc_error( + result, + 0, + anchor_lang::error::ErrorCode::InvalidProgramId.into(), + ) + .unwrap(); + } + // 4. failing invalid mint + { + let result = add_token_pool( + &mut rpc, + &payer, + &mint, + None, + current_token_pool_bump, + is_token_22, + FailingTestsAddTokenPool::InvalidMint, + ) + .await; + assert_rpc_error( + result, + 0, + anchor_lang::error::ErrorCode::AccountNotInitialized.into(), + ) + .unwrap(); + } + // 5. failing inconsistent mints + { + let result = add_token_pool( + &mut rpc, + &payer, + &mint, + Some(invalid_mint), + current_token_pool_bump, + is_token_22, + FailingTestsAddTokenPool::InconsistentMints, + ) + .await; + assert_rpc_error(result, 0, ErrorCode::InvalidTokenPoolPda.into()).unwrap(); + } + // 6. failing invalid program id + { + let result = add_token_pool( + &mut rpc, + &payer, + &mint, + None, + current_token_pool_bump, + is_token_22, + FailingTestsAddTokenPool::InvalidTokenProgramId, + ) + .await; + assert_rpc_error( + result, + 0, + anchor_lang::error::ErrorCode::InvalidProgramId.into(), + ) + .unwrap(); + } + // 7. failing invalid cpi authority pda + { + let result = add_token_pool( + &mut rpc, + &payer, + &mint, + None, + current_token_pool_bump, + is_token_22, + FailingTestsAddTokenPool::InvalidCpiAuthorityPda, + ) + .await; + assert_rpc_error( + result, + 0, + anchor_lang::error::ErrorCode::ConstraintSeeds.into(), + ) + .unwrap(); + } + // create all remaining token pools + create_additional_token_pools(&mut rpc, &payer, &mint, is_token_22, 5) + .await + .unwrap(); + // 8. failing invalid token pool bump (too large) + { + let result = add_token_pool( + &mut rpc, + &payer, + &mint, + None, + NUM_MAX_POOL_ACCOUNTS, + is_token_22, + FailingTestsAddTokenPool::Functional, + ) + .await; + assert_rpc_error(result, 0, ErrorCode::InvalidTokenPoolBump.into()).unwrap(); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum FailingTestsAddTokenPool { + Functional, + InvalidMint, + InconsistentMints, + InvalidTokenPoolPda, + InvalidSystemProgramId, + InvalidExistingTokenPoolPda, + InvalidCpiAuthorityPda, + InvalidTokenProgramId, +} + +pub async fn add_token_pool( + rpc: &mut R, + fee_payer: &Keypair, + mint: &Pubkey, + invalid_mint: Option, + token_pool_bump: u8, + is_token_22: bool, + mode: FailingTestsAddTokenPool, +) -> Result { + let token_pool_pda = if mode == FailingTestsAddTokenPool::InvalidTokenPoolPda { + Pubkey::new_unique() + } else { + get_token_pool_pda_with_bump(mint, token_pool_bump) + }; + let existing_token_pool_pda = if mode == FailingTestsAddTokenPool::InvalidExistingTokenPoolPda { + get_token_pool_pda_with_bump(mint, token_pool_bump.saturating_sub(2)) + } else if let Some(invalid_mint) = invalid_mint { + get_token_pool_pda_with_bump(&invalid_mint, token_pool_bump.saturating_sub(1)) + } else { + get_token_pool_pda_with_bump(mint, token_pool_bump.saturating_sub(1)) + }; + let instruction_data = light_compressed_token::instruction::AddTokenPool { token_pool_bump }; + + let token_program: Pubkey = if mode == FailingTestsAddTokenPool::InvalidTokenProgramId { + Pubkey::new_unique() + } else if is_token_22 { + anchor_spl::token_2022::ID + } else { + anchor_spl::token::ID + }; + let cpi_authority_pda = if mode == FailingTestsAddTokenPool::InvalidCpiAuthorityPda { + Pubkey::new_unique() + } else { + get_cpi_authority_pda().0 + }; + let system_program = if mode == FailingTestsAddTokenPool::InvalidSystemProgramId { + Pubkey::new_unique() + } else { + system_program::ID + }; + let mint = if mode == FailingTestsAddTokenPool::InvalidMint { + Pubkey::new_unique() + } else { + *mint + }; + + let accounts = light_compressed_token::accounts::AddTokenPoolInstruction { + fee_payer: fee_payer.pubkey(), + token_pool_pda, + system_program, + mint, + token_program, + cpi_authority_pda, + existing_token_pool_pda, + }; + + let instruction = Instruction { + program_id: light_compressed_token::ID, + accounts: accounts.to_account_metas(Some(true)), + data: instruction_data.data(), + }; + rpc.create_and_send_transaction(&[instruction], &fee_payer.pubkey(), &[fee_payer]) + .await +} + #[serial] #[tokio::test] async fn test_wrapped_sol() { @@ -391,6 +642,8 @@ async fn test_wrapped_sol() { &token_account_keypair.pubkey(), None, is_token_22, + 0, + None, ) .await; let input_compressed_accounts = @@ -405,6 +658,8 @@ async fn test_wrapped_sol() { &token_account_keypair.pubkey(), None, is_token_22, + 0, + None, ) .await; } @@ -500,6 +755,7 @@ async fn compress_spl_account() { &merkle_tree_pubkey, None, is_token_22, + 0, ) .await .unwrap(); @@ -526,6 +782,7 @@ async fn compress_spl_account() { &merkle_tree_pubkey, Some(first_token_account_balance + 1), // invalid remaining amount is_token_22, + 0, ) .await; assert_rpc_error(result, 0, ErrorCode::InsufficientTokenAccountBalance.into()).unwrap(); @@ -540,6 +797,7 @@ async fn compress_spl_account() { &merkle_tree_pubkey, Some(1), is_token_22, + 0, ) .await .unwrap(); @@ -703,6 +961,7 @@ async fn test_mint_to_failing() { recipients.clone(), None, is_token_22, + 0, ); let result = rpc .create_and_send_transaction(&[instruction], &payer_2.pubkey(), &[&payer_2]) @@ -722,6 +981,7 @@ async fn test_mint_to_failing() { recipients.clone(), None, is_token_22, + 0, ); let result = rpc .create_and_send_transaction(&[instruction], &payer_1.pubkey(), &[&payer_1]) @@ -772,12 +1032,7 @@ async fn test_mint_to_failing() { let result = rpc .create_and_send_transaction(&[instruction], &payer_1.pubkey(), &[&payer_1]) .await; - assert_rpc_error( - result, - 0, - anchor_lang::error::ErrorCode::ConstraintSeeds.into(), - ) - .unwrap(); + assert_rpc_error(result, 0, ErrorCode::InvalidTokenPoolPda.into()).unwrap(); } // 4. Try to mint token from `mint_2` while using `mint_1` pool. { @@ -812,12 +1067,7 @@ async fn test_mint_to_failing() { let result = rpc .create_and_send_transaction(&[instruction], &payer_2.pubkey(), &[&payer_2]) .await; - assert_rpc_error( - result, - 0, - anchor_lang::error::ErrorCode::ConstraintSeeds.into(), - ) - .unwrap(); + assert_rpc_error(result, 0, ErrorCode::InvalidTokenPoolPda.into()).unwrap(); } // 5. Invalid CPI authority. { @@ -989,6 +1239,7 @@ async fn test_mint_to_failing() { recipients.clone(), None, is_token_22, + 0, ); let result = rpc .create_and_send_transaction(&[instruction], &payer_1.pubkey(), &[&payer_1]) @@ -1016,6 +1267,7 @@ async fn test_mint_to_failing() { recipients.clone(), None, is_token_22, + 0, ); let result = rpc .create_and_send_transaction(&[instruction], &payer_1.pubkey(), &[&payer_1]) @@ -1034,6 +1286,7 @@ async fn test_mint_to_failing() { recipients.clone(), None, is_token_22, + 0, ); // The first mint is still below `u64::MAX`. rpc.create_and_send_transaction(&[instruction.clone()], &payer_1.pubkey(), &[&payer_1]) @@ -1207,7 +1460,7 @@ async fn perform_transfer_22_test( #[tokio::test] async fn test_decompression() { spawn_prover( - false, + true, ProverConfig { run_mode: None, circuits: vec![ProofType::Inclusion], @@ -1269,6 +1522,8 @@ async fn test_decompression() { &token_account_keypair.pubkey(), None, is_token_22, + 0, + None, ) .await; println!("5"); @@ -1282,12 +1537,314 @@ async fn test_decompression() { &token_account_keypair.pubkey(), None, is_token_22, + 0, + None, ) .await; } kill_prover(); } +pub async fn mint_tokens_to_all_token_pools( + rpc: &mut R, + test_indexer: &mut TestIndexer, + merkle_tree_pubkey: &Pubkey, + mint_authority: &Keypair, + mint: &Pubkey, + amounts: Vec, + recipients: Vec, + is_token22: bool, + invert_order: bool, +) -> Result<(), RpcError> { + let iterator = (0..NUM_MAX_POOL_ACCOUNTS).collect::>(); + let iterator = if invert_order { + iterator.iter().rev().cloned().collect::>() + } else { + iterator + }; + for bump in iterator { + let token_pool_pda = get_token_pool_pda_with_bump(mint, bump); + let token_pool_account = rpc.get_account(token_pool_pda).await?; + if token_pool_account.is_some() { + mint_tokens_22_helper_with_lamports_and_bump( + rpc, + test_indexer, + merkle_tree_pubkey, + mint_authority, + mint, + amounts.clone(), + recipients.clone(), + None, + is_token22, + bump, + ) + .await; + } + } + Ok(()) +} +use anchor_lang::AccountDeserialize; +/// Assert that every token pool account contains `amount` tokens. +pub async fn assert_minted_to_all_token_pools( + rpc: &mut R, + amount: u64, + mint: &Pubkey, +) -> Result<(), RpcError> { + for bump in 0..NUM_MAX_POOL_ACCOUNTS { + let token_pool_pda = get_token_pool_pda_with_bump(&mint, bump); + let mut token_pool_account = rpc.get_account(token_pool_pda).await?.unwrap(); + let token_pool_data = + TokenAccount::try_deserialize_unchecked(&mut &*token_pool_account.data.as_mut_slice()) + .unwrap(); + assert_eq!(token_pool_data.amount, amount); + } + + Ok(()) +} + +#[serial] +#[tokio::test] +async fn test_mint_to_and_burn_from_all_token_pools() { + spawn_prover( + true, + ProverConfig { + run_mode: None, + circuits: vec![ProofType::Inclusion], + }, + ) + .await; + for is_token_22 in vec![false, true] { + let (mut rpc, env) = setup_test_programs_with_accounts(None).await; + let payer = rpc.get_payer().insecure_clone(); + let merkle_tree_pubkey = env.merkle_tree_pubkey; + let mut test_indexer = + TestIndexer::::init_from_env(&payer, &env, None).await; + let mint = if is_token_22 { + create_mint_22_helper(&mut rpc, &payer).await + } else { + create_mint_helper(&mut rpc, &payer).await + }; + create_additional_token_pools(&mut rpc, &payer, &mint, is_token_22, NUM_MAX_POOL_ACCOUNTS) + .await + .unwrap(); + let amount = 123; + mint_tokens_to_all_token_pools( + &mut rpc, + &mut test_indexer, + &merkle_tree_pubkey, + &payer, + &mint, + vec![amount], + vec![payer.pubkey()], + is_token_22, + is_token_22, // invert order + ) + .await + .unwrap(); + assert_minted_to_all_token_pools(&mut rpc, amount, &mint) + .await + .unwrap(); + let iterator = (0..NUM_MAX_POOL_ACCOUNTS).collect::>(); + let iterator = if !is_token_22 { + iterator.iter().rev().cloned().collect::>() + } else { + iterator + }; + for i in iterator { + let input_compressed_account = + test_indexer.get_compressed_token_accounts_by_owner(&payer.pubkey())[0].clone(); + let change_account_merkle_tree = input_compressed_account + .compressed_account + .merkle_context + .merkle_tree_pubkey; + burn_test( + &payer, + &mut rpc, + &mut test_indexer, + vec![input_compressed_account], + &change_account_merkle_tree, + amount, + false, + None, + is_token_22, + i, + ) + .await; + } + assert_minted_to_all_token_pools(&mut rpc, 0, &mint) + .await + .unwrap(); + } +} + +#[serial] +#[tokio::test] +async fn test_multiple_decompression() { + spawn_prover( + true, + ProverConfig { + run_mode: None, + circuits: vec![ProofType::Inclusion], + }, + ) + .await; + let rng = &mut rand::thread_rng(); + for is_token_22 in vec![false, true] { + println!("is_token_22: {}", is_token_22); + let (mut context, env) = setup_test_programs_with_accounts(None).await; + let payer = context.get_payer().insecure_clone(); + let merkle_tree_pubkey = env.merkle_tree_pubkey; + let mut test_indexer = + TestIndexer::::init_from_env(&payer, &env, None).await; + let sender = Keypair::new(); + airdrop_lamports(&mut context, &sender.pubkey(), 1_000_000_000) + .await + .unwrap(); + let mint = if is_token_22 { + create_mint_22_helper(&mut context, &payer).await + } else { + create_mint_helper(&mut context, &payer).await + }; + let amount = 10000u64; + create_additional_token_pools( + &mut context, + &payer, + &mint, + is_token_22, + NUM_MAX_POOL_ACCOUNTS, + ) + .await + .unwrap(); + + mint_tokens_to_all_token_pools( + &mut context, + &mut test_indexer, + &merkle_tree_pubkey, + &payer, + &mint, + vec![amount], + vec![sender.pubkey()], + is_token_22, + is_token_22, + ) + .await + .unwrap(); + println!("3"); + let token_account_keypair = Keypair::new(); + create_token_2022_account( + &mut context, + &mint, + &token_account_keypair, + &sender, + is_token_22, + ) + .await + .unwrap(); + println!("4"); + + // 1. functional - decompress from any token pool + let mut iterator = vec![0, 1, 2, 3, 4]; + iterator.shuffle(rng); + for i in iterator { + let input_compressed_account = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey()) + .iter() + .filter(|x| x.token_data.amount != 0) + .collect::>()[0] + .clone(); + println!("i = {}", i); + println!("input_compressed_account = {:?}", input_compressed_account); + decompress_test( + &sender, + &mut context, + &mut test_indexer, + vec![input_compressed_account], + amount, + &merkle_tree_pubkey, + &token_account_keypair.pubkey(), + None, + is_token_22, + i, + None, + ) + .await; + } + + println!("5"); + + // 2. functional - compress to any token pool + let mut iterator = vec![0, 1, 2, 3, 4]; + iterator.shuffle(rng); + for i in iterator { + compress_test( + &sender, + &mut context, + &mut test_indexer, + amount, + &mint, + &merkle_tree_pubkey, + &token_account_keypair.pubkey(), + None, + is_token_22, + i, + None, + ) + .await; + } + + // Decompress from all token pools + { + let input_compressed_accounts = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey())[0..4] + .to_vec(); + let amount = input_compressed_accounts + .iter() + .map(|x| x.token_data.amount) + .sum(); + let mut add_token_pool_accounts = (0..4) + .map(|x| get_token_pool_pda_with_bump(&mint, x.clone())) + .collect::>(); + add_token_pool_accounts.shuffle(rng); + decompress_test( + &sender, + &mut context, + &mut test_indexer, + input_compressed_accounts, + amount, + &merkle_tree_pubkey, + &token_account_keypair.pubkey(), + None, + is_token_22, + 4, + Some(add_token_pool_accounts.clone()), + ) + .await; + let input_compressed_accounts = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey()) + .iter() + .filter(|x| x.token_data.amount != 0) + .collect::>()[0] + .clone(); + let amount = input_compressed_accounts.token_data.amount; + decompress_test( + &sender, + &mut context, + &mut test_indexer, + vec![input_compressed_accounts], + amount, + &merkle_tree_pubkey, + &token_account_keypair.pubkey(), + None, + is_token_22, + 4, + Some(add_token_pool_accounts), + ) + .await; + } + } + kill_prover(); +} + /// Test delegation: /// 1. Delegate tokens with approve /// 2. Delegate transfers a part of the delegated tokens @@ -2293,6 +2850,9 @@ async fn test_burn() { create_mint_helper(&mut rpc, &payer).await }; let amount = 10000u64; + create_additional_token_pools(&mut rpc, &payer, &mint, is_token_22, NUM_MAX_POOL_ACCOUNTS) + .await + .unwrap(); mint_tokens_22_helper_with_lamports( &mut rpc, &mut test_indexer, @@ -2324,6 +2884,7 @@ async fn test_burn() { false, None, is_token_22, + 0, ) .await; } @@ -2356,56 +2917,156 @@ async fn test_burn() { test_indexer.get_compressed_token_accounts_by_owner(&sender.pubkey()); let input_compressed_accounts = input_compressed_accounts .iter() - .filter(|x| x.token_data.delegate.is_some()) + .filter(|x| x.token_data.delegate.is_some()) + .cloned() + .collect::>(); + let burn_amount = 100; + let change_account_merkle_tree = input_compressed_accounts[0] + .compressed_account + .merkle_context + .merkle_tree_pubkey; + burn_test( + &delegate, + &mut rpc, + &mut test_indexer, + input_compressed_accounts, + &change_account_merkle_tree, + burn_amount, + true, + None, + is_token_22, + 0, + ) + .await; + } + // 3. Burn all delegated tokens + { + let input_compressed_accounts = + test_indexer.get_compressed_token_accounts_by_owner(&sender.pubkey()); + let input_compressed_accounts = input_compressed_accounts + .iter() + .filter(|x| x.token_data.delegate.is_some()) + .cloned() + .collect::>(); + let burn_amount = input_compressed_accounts + .iter() + .map(|x| x.token_data.amount) + .sum::(); + let change_account_merkle_tree = input_compressed_accounts[0] + .compressed_account + .merkle_context + .merkle_tree_pubkey; + burn_test( + &delegate, + &mut rpc, + &mut test_indexer, + input_compressed_accounts, + &change_account_merkle_tree, + burn_amount, + true, + None, + is_token_22, + 0, + ) + .await; + } + // 5. Burn tokens from multiple token pools + { + let amount = 123; + mint_tokens_to_all_token_pools( + &mut rpc, + &mut test_indexer, + &env.merkle_tree_pubkey, + &payer, + &mint, + vec![amount], + vec![sender.pubkey()], + is_token_22, + false, + ) + .await + .unwrap(); + let input_compressed_accounts = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey()) + .iter() + .filter(|x| x.token_data.amount != 0) .cloned() - .collect::>(); - let burn_amount = 100; - let change_account_merkle_tree = input_compressed_accounts[0] + .collect::>()[0..4] + .to_vec(); + let burn_amount = input_compressed_accounts + .iter() + .map(|x| x.token_data.amount) + .sum(); + let invalid_change_account_merkle_tree = input_compressed_accounts[0] .compressed_account .merkle_context - .merkle_tree_pubkey; - burn_test( - &delegate, + .nullifier_queue_pubkey; + let mut additional_token_pool_accounts = (0..4) + .map(|x| get_token_pool_pda_with_bump(&mint, x)) + .collect::>(); + let rng = &mut thread_rng(); + additional_token_pool_accounts.shuffle(rng); + let (_, _, _, _, instruction) = create_burn_test_instruction( + &sender, &mut rpc, &mut test_indexer, - input_compressed_accounts, - &change_account_merkle_tree, + input_compressed_accounts.as_slice(), + &invalid_change_account_merkle_tree, burn_amount, - true, - None, + false, + BurnInstructionMode::Normal, is_token_22, + 4, + Some(additional_token_pool_accounts.clone()), ) .await; - } - // 3. Burn all delegated tokens - { - let input_compressed_accounts = - test_indexer.get_compressed_token_accounts_by_owner(&sender.pubkey()); - let input_compressed_accounts = input_compressed_accounts + + let (event, _, _) = rpc + .create_and_send_transaction_with_event( + &[instruction], + &payer.pubkey(), + &[&payer, &sender], + None, + ) + .await + .unwrap() + .unwrap(); + let slot = rpc.get_slot().await.unwrap(); + test_indexer.add_event_and_compressed_accounts(slot, &event); + let input_compressed_accounts = test_indexer + .get_compressed_token_accounts_by_owner(&sender.pubkey()) .iter() - .filter(|x| x.token_data.delegate.is_some()) + .filter(|x| x.token_data.amount != 0) .cloned() - .collect::>(); + .collect::>(); let burn_amount = input_compressed_accounts .iter() .map(|x| x.token_data.amount) - .sum::(); - let change_account_merkle_tree = input_compressed_accounts[0] - .compressed_account - .merkle_context - .merkle_tree_pubkey; - burn_test( - &delegate, + .sum(); + + additional_token_pool_accounts.shuffle(rng); + + let (_, _, _, _, instruction) = create_burn_test_instruction( + &sender, &mut rpc, &mut test_indexer, - input_compressed_accounts, - &change_account_merkle_tree, + &input_compressed_accounts, + &merkle_tree_pubkey, burn_amount, - true, - None, + false, + BurnInstructionMode::Normal, is_token_22, + 4, + Some(additional_token_pool_accounts), ) .await; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &sender]) + .await + .unwrap(); + assert_minted_to_all_token_pools(&mut rpc, 0, &mint) + .await + .unwrap(); } } } @@ -2495,6 +3156,8 @@ async fn failing_tests_burn() { false, BurnInstructionMode::InvalidProof, is_token_22, + 0, + None, ) .await; let res = rpc @@ -2521,6 +3184,8 @@ async fn failing_tests_burn() { true, BurnInstructionMode::Normal, is_token_22, + 0, + None, ) .await; let res = rpc @@ -2556,6 +3221,8 @@ async fn failing_tests_burn() { true, BurnInstructionMode::Normal, is_token_22, + 0, + None, ) .await; let res = rpc @@ -2582,6 +3249,8 @@ async fn failing_tests_burn() { false, BurnInstructionMode::Normal, is_token_22, + 0, + None, ) .await; let res = rpc @@ -2612,6 +3281,8 @@ async fn failing_tests_burn() { false, BurnInstructionMode::InvalidMint, is_token_22, + 0, + None, ) .await; let res = rpc @@ -2643,6 +3314,8 @@ async fn failing_tests_burn() { false, BurnInstructionMode::Normal, is_token_22, + 0, + None, ) .await; let res = rpc @@ -2655,6 +3328,65 @@ async fn failing_tests_burn() { ) .unwrap(); } + // 6. invalid token pool (not initialized) + { + let input_compressed_accounts = + test_indexer.get_compressed_token_accounts_by_owner(&sender.pubkey()); + let burn_amount = 1; + let invalid_change_account_merkle_tree = input_compressed_accounts[0] + .compressed_account + .merkle_context + .nullifier_queue_pubkey; + let (_, _, _, _, instruction) = create_burn_test_instruction( + &sender, + &mut rpc, + &mut test_indexer, + &input_compressed_accounts, + &invalid_change_account_merkle_tree, + burn_amount, + false, + BurnInstructionMode::Normal, + is_token_22, + 1, + None, + ) + .await; + let res = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &sender]) + .await; + assert_rpc_error(res, 0, ErrorCode::InvalidTokenPoolPda.into()).unwrap(); + } + // 7. invalid token pool (invalid mint) + { + let input_compressed_accounts = + test_indexer.get_compressed_token_accounts_by_owner(&sender.pubkey()); + let burn_amount = 1; + let invalid_change_account_merkle_tree = input_compressed_accounts[0] + .compressed_account + .merkle_context + .nullifier_queue_pubkey; + let (_, _, _, _, mut instruction) = create_burn_test_instruction( + &sender, + &mut rpc, + &mut test_indexer, + &input_compressed_accounts, + &invalid_change_account_merkle_tree, + burn_amount, + false, + BurnInstructionMode::Normal, + is_token_22, + 0, + None, + ) + .await; + let mint = create_mint_helper(&mut rpc, &payer).await; + let token_pool = get_token_pool_pda(&mint); + instruction.accounts[4] = AccountMeta::new(token_pool, false); + let res = rpc + .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &sender]) + .await; + assert_rpc_error(res, 0, ErrorCode::InvalidTokenPoolPda.into()).unwrap(); + } } } @@ -3378,9 +4110,15 @@ async fn test_failing_thaw() { /// 4. Invalid decompression amount +1 /// 5. Invalid decompression amount 0 /// 6: invalid token recipient -/// 7. Invalid compression amount -1 -/// 8. Invalid compression amount +1 -/// 9. Invalid compression amount 0 +/// 7. invalid token pool pda (in struct) +/// 8. invalid token pool pda (in remaining accounts) +/// 9. FailedToDecompress pass multiple correct token accounts with insufficient balance +/// 10. invalid token pool pda from invalid mint (in struct) +/// 11. invalid token pool pda from invalid mint (in remaining accounts) +/// 12. Invalid compression amount -1 +/// 13. Invalid compression amount +1 +/// 14. Invalid compression amount 0 +/// 15. Invalid token pool pda compress (in struct) #[serial] #[tokio::test] async fn test_failing_decompression() { @@ -3407,6 +4145,15 @@ async fn test_failing_decompression() { } else { create_mint_helper(&mut context, &payer).await }; + create_additional_token_pools( + &mut context, + &payer, + &mint, + is_token_22, + NUM_MAX_POOL_ACCOUNTS, + ) + .await + .unwrap(); let amount = 10000u64; mint_tokens_22_helper_with_lamports( &mut context, @@ -3450,6 +4197,7 @@ async fn test_failing_decompression() { &mint, 0, //ProgramError::InvalidAccountData.into(), error code 17179869184 does not fit u32 is_token_22, + None, ) .await .unwrap_err(); @@ -3480,6 +4228,7 @@ async fn test_failing_decompression() { &mint, ErrorCode::InvalidTokenPoolPda.into(), is_token_22, + None, ) .await .unwrap(); @@ -3508,6 +4257,7 @@ async fn test_failing_decompression() { &mint, ErrorCode::InvalidTokenPoolPda.into(), is_token_22, + None, ) .await .unwrap(); @@ -3528,6 +4278,7 @@ async fn test_failing_decompression() { &mint, ErrorCode::SumCheckFailed.into(), is_token_22, + None, ) .await .unwrap(); @@ -3548,6 +4299,7 @@ async fn test_failing_decompression() { &mint, ErrorCode::ComputeOutputSumFailed.into(), is_token_22, + None, ) .await .unwrap(); @@ -3568,6 +4320,7 @@ async fn test_failing_decompression() { &mint, ErrorCode::SumCheckFailed.into(), is_token_22, + None, ) .await .unwrap(); @@ -3588,6 +4341,133 @@ async fn test_failing_decompression() { &mint, ErrorCode::IsTokenPoolPda.into(), is_token_22, + None, + ) + .await + .unwrap(); + } + // Test 7: invalid token pool pda (in struct) + { + failing_compress_decompress( + &sender, + &mut context, + &mut test_indexer, + input_compressed_account.clone(), + decompress_amount, // needs to be consistent with compression amount + &merkle_tree_pubkey, + decompress_amount, + false, + &token_account_keypair.pubkey(), + Some(get_token_pool_pda_with_bump(&mint, NUM_MAX_POOL_ACCOUNTS)), + &mint, + anchor_lang::error::ErrorCode::AccountNotInitialized.into(), + is_token_22, + None, + ) + .await + .unwrap(); + } + // Test 8: invalid token pool pda (in remaining accounts) + { + failing_compress_decompress( + &sender, + &mut context, + &mut test_indexer, + input_compressed_account.clone(), + decompress_amount, // needs to be consistent with compression amount + &merkle_tree_pubkey, + decompress_amount, + false, + &token_account_keypair.pubkey(), + Some(get_token_pool_pda_with_bump(&mint, 3)), + &mint, + ErrorCode::InvalidTokenPoolPda.into(), + is_token_22, + Some(vec![get_token_pool_pda_with_bump( + &mint, + NUM_MAX_POOL_ACCOUNTS, + )]), + ) + .await + .unwrap(); + } + // Test 9: FailedToDecompress pass multiple correct token accounts with insufficient balance + { + let token_pool = get_token_pool_pda_with_bump(&mint, 3); + let mut account = context.get_account(token_pool).await.unwrap().unwrap(); + println!("token pool account {:?}", token_pool); + let amount = + TokenAccount::try_deserialize_unchecked(&mut &*account.data.as_mut_slice()) + .unwrap() + .amount; + println!("{:?}", amount); + failing_compress_decompress( + &sender, + &mut context, + &mut test_indexer, + input_compressed_account.clone(), + decompress_amount, // needs to be consistent with compression amount + &merkle_tree_pubkey, + decompress_amount, + false, + &token_account_keypair.pubkey(), + Some(token_pool), + &mint, + ErrorCode::FailedToDecompress.into(), + is_token_22, + Some(vec![ + token_pool, + get_token_pool_pda_with_bump(&mint, 1), + get_token_pool_pda_with_bump(&mint, 2), + get_token_pool_pda_with_bump(&mint, 4), + ]), + ) + .await + .unwrap(); + } + + let invalid_mint = create_mint_22_helper(&mut context, &payer).await; + // Test 10: invalid token pool pda from invalid mint (in struct) + { + failing_compress_decompress( + &sender, + &mut context, + &mut test_indexer, + input_compressed_account.clone(), + decompress_amount, // needs to be consistent with compression amount + &merkle_tree_pubkey, + decompress_amount, + false, + &token_account_keypair.pubkey(), + Some(get_token_pool_pda_with_bump(&invalid_mint, 0)), + &mint, + ErrorCode::InvalidTokenPoolPda.into(), + is_token_22, + Some(vec![get_token_pool_pda_with_bump( + &mint, + NUM_MAX_POOL_ACCOUNTS, + )]), + ) + .await + .unwrap(); + } + // Test 11: invalid token pool pda from invalid mint (in remaining accounts) + { + failing_compress_decompress( + &sender, + &mut context, + &mut test_indexer, + input_compressed_account.clone(), + decompress_amount, // needs to be consistent with compression amount + &merkle_tree_pubkey, + decompress_amount, + false, + &token_account_keypair.pubkey(), + Some(get_token_pool_pda_with_bump(&mint, 4)), + &mint, + ErrorCode::InvalidTokenPoolPda.into(), + is_token_22, + Some(vec![get_token_pool_pda_with_bump(&invalid_mint, 0)]), ) .await .unwrap(); @@ -3604,10 +4484,12 @@ async fn test_failing_decompression() { &token_account_keypair.pubkey(), None, is_token_22, + 0, + None, ) .await; let compress_amount = decompress_amount - 100; - // Test 7: invalid compression amount -1 + // Test 12: invalid compression amount -1 { failing_compress_decompress( &sender, @@ -3623,11 +4505,12 @@ async fn test_failing_decompression() { &mint, ErrorCode::ComputeOutputSumFailed.into(), is_token_22, + None, ) .await .unwrap(); } - // Test 8: invalid compression amount +1 + // Test 13: invalid compression amount +1 { failing_compress_decompress( &sender, @@ -3643,11 +4526,12 @@ async fn test_failing_decompression() { &mint, ErrorCode::SumCheckFailed.into(), is_token_22, + None, ) .await .unwrap(); } - // Test 9: invalid compression amount 0 + // Test 14: invalid compression amount 0 { failing_compress_decompress( &sender, @@ -3663,6 +4547,28 @@ async fn test_failing_decompression() { &mint, ErrorCode::ComputeOutputSumFailed.into(), is_token_22, + None, + ) + .await + .unwrap(); + } + // Test 15: invalid token pool pda (in struct) + { + failing_compress_decompress( + &sender, + &mut context, + &mut test_indexer, + Vec::new(), + compress_amount, // needs to be consistent with compression amount + &merkle_tree_pubkey, + compress_amount, + true, + &token_account_keypair.pubkey(), + Some(get_token_pool_pda_with_bump(&invalid_mint, 0)), + &mint, + ErrorCode::InvalidTokenPoolPda.into(), + is_token_22, + None, ) .await .unwrap(); @@ -3678,6 +4584,8 @@ async fn test_failing_decompression() { &token_account_keypair.pubkey(), None, is_token_22, + 0, + None, ) .await; } @@ -3699,6 +4607,7 @@ pub async fn failing_compress_decompress( mint: &Pubkey, error_code: u32, is_token_22: bool, + additional_token_pools: Option>, ) -> Result<(), RpcError> { let max_amount: u64 = input_compressed_accounts .iter() @@ -3772,6 +4681,7 @@ pub async fn failing_compress_decompress( None, None, is_token_22, + &additional_token_pools.unwrap_or_default(), ) .unwrap(); let instructions = if !is_compress { @@ -4237,6 +5147,7 @@ async fn perform_transfer_failing_test( None, None, false, + &[], ) .unwrap(); diff --git a/test-programs/system-cpi-test/tests/test_program_owned_trees.rs b/test-programs/system-cpi-test/tests/test_program_owned_trees.rs index 572399cce..e270f45ab 100644 --- a/test-programs/system-cpi-test/tests/test_program_owned_trees.rs +++ b/test-programs/system-cpi-test/tests/test_program_owned_trees.rs @@ -95,6 +95,7 @@ async fn test_program_owned_merkle_tree() { vec![recipient_keypair.pubkey(); 1], None, false, + 0, ); let pre_merkle_tree = get_concurrent_merkle_tree::< StateMerkleTreeAccount, @@ -160,6 +161,7 @@ async fn test_program_owned_merkle_tree() { vec![recipient_keypair.pubkey(); 1], None, false, + 0, ); let latest_blockhash = rpc.get_latest_blockhash().await.unwrap(); diff --git a/test-utils/src/assert_token_tx.rs b/test-utils/src/assert_token_tx.rs index a4a07c67b..82846ca42 100644 --- a/test-utils/src/assert_token_tx.rs +++ b/test-utils/src/assert_token_tx.rs @@ -1,10 +1,7 @@ use anchor_lang::AnchorSerialize; use forester_utils::indexer::{Indexer, TokenDataWithContext}; use light_client::rpc::RpcConnection; -use light_compressed_token::{ - get_token_pool_pda, - process_transfer::{get_cpi_authority_pda, TokenTransferOutputData}, -}; +use light_compressed_token::process_transfer::{get_cpi_authority_pda, TokenTransferOutputData}; use light_system_program::sdk::{ compressed_account::CompressedAccountWithMerkleContext, event::PublicTransactionEvent, }; @@ -201,6 +198,7 @@ pub async fn assert_mint_to<'a, R: RpcConnection, I: Indexer>( created_token_accounts: &[TokenDataWithContext], previous_mint_supply: u64, previous_sol_pool_amount: u64, + token_pool_pda: Pubkey, ) { let mut created_token_accounts = created_token_accounts.to_vec(); for (recipient, amount) in recipients.iter().zip(amounts) { @@ -222,10 +220,10 @@ pub async fn assert_mint_to<'a, R: RpcConnection, I: Indexer>( let sum_amounts = amounts.iter().sum::(); assert_eq!(mint_account.supply, previous_mint_supply + sum_amounts); - let pool = get_token_pool_pda(&mint); - let pool_account = - spl_token::state::Account::unpack(&rpc.get_account(pool).await.unwrap().unwrap().data) - .unwrap(); + let pool_account = spl_token::state::Account::unpack( + &rpc.get_account(token_pool_pda).await.unwrap().unwrap().data, + ) + .unwrap(); assert_eq!(pool_account.amount, previous_sol_pool_amount + sum_amounts); } diff --git a/test-utils/src/e2e_test_env.rs b/test-utils/src/e2e_test_env.rs index 1b1d1b511..d795de4cc 100644 --- a/test-utils/src/e2e_test_env.rs +++ b/test-utils/src/e2e_test_env.rs @@ -1839,6 +1839,7 @@ where false, transaction_paramets, false, + 0, ) .await; self.stats.spl_burned += 1; @@ -1984,6 +1985,8 @@ where &token_account, transaction_paramets, false, + 0, // TODO: make random + None, ) .await; self.stats.spl_compress += 1; @@ -2061,6 +2064,8 @@ where &token_account, transaction_paramets, false, + 0, // TODO: make random + None, ) .await; self.stats.spl_decompress += 1; diff --git a/test-utils/src/spl.rs b/test-utils/src/spl.rs index a6a2b88ee..df672e52d 100644 --- a/test-utils/src/spl.rs +++ b/test-utils/src/spl.rs @@ -9,13 +9,17 @@ use light_client::{ }; use light_compressed_token::{ burn::sdk::{create_burn_instruction, CreateBurnInstructionInputs}, + constants::NUM_MAX_POOL_ACCOUNTS, delegation::sdk::{ create_approve_instruction, create_revoke_instruction, CreateApproveInstructionInputs, CreateRevokeInstructionInputs, }, freeze::sdk::{create_instruction, CreateInstructionInputs}, - get_token_pool_pda, - mint_sdk::{create_create_token_pool_instruction, create_mint_to_instruction}, + get_token_pool_pda, get_token_pool_pda_with_bump, + mint_sdk::{ + create_add_token_pool_instruction, create_create_token_pool_instruction, + create_mint_to_instruction, + }, process_compress_spl_token_account::sdk::create_compress_spl_token_account_instruction, process_transfer::{transfer_sdk::create_transfer_instruction, TokenTransferOutputData}, token_data::AccountState, @@ -135,6 +139,34 @@ pub async fn mint_tokens_22_helper_with_lamports recipients: Vec, lamports: Option, token_22: bool, +) { + mint_tokens_22_helper_with_lamports_and_bump( + rpc, + test_indexer, + merkle_tree_pubkey, + mint_authority, + mint, + amounts, + recipients, + lamports, + token_22, + 0, + ) + .await +} + +#[allow(clippy::too_many_arguments)] +pub async fn mint_tokens_22_helper_with_lamports_and_bump>( + rpc: &mut R, + test_indexer: &mut I, + merkle_tree_pubkey: &Pubkey, + mint_authority: &Keypair, + mint: &Pubkey, + amounts: Vec, + recipients: Vec, + lamports: Option, + token_22: bool, + token_pool_bump: u8, ) { let payer_pubkey = mint_authority.pubkey(); let instruction = create_mint_to_instruction( @@ -146,6 +178,7 @@ pub async fn mint_tokens_22_helper_with_lamports recipients.clone(), lamports, token_22, + token_pool_bump, ); let output_merkle_tree_accounts = @@ -157,7 +190,7 @@ pub async fn mint_tokens_22_helper_with_lamports .unwrap() .supply; - let pool: Pubkey = get_token_pool_pda(mint); + let pool: Pubkey = get_token_pool_pda_with_bump(mint, token_pool_bump); let previous_pool_amount = spl_token::state::Account::unpack(&rpc.get_account(pool).await.unwrap().unwrap().data) .unwrap() @@ -185,6 +218,7 @@ pub async fn mint_tokens_22_helper_with_lamports &created_token_accounts, previous_mint_supply, previous_pool_amount, + pool, ) .await; } @@ -341,6 +375,40 @@ pub fn create_initialize_mint_22_instructions( ) } +pub async fn create_additional_token_pools( + rpc: &mut R, + payer: &Keypair, + mint: &Pubkey, + is_token_22: bool, + num: u8, +) -> Result, RpcError> { + let mut instructions = Vec::new(); + let mut created_token_pools = Vec::new(); + + for token_pool_bump in 0..NUM_MAX_POOL_ACCOUNTS { + if instructions.len() == num as usize { + break; + } + let token_pool_pda = get_token_pool_pda_with_bump(mint, token_pool_bump); + let account = rpc.get_account(token_pool_pda).await.unwrap(); + println!("bump {}", token_pool_bump); + println!("account exists {:?}", account.is_some()); + if account.is_none() { + created_token_pools.push(token_pool_pda); + let instruction = create_add_token_pool_instruction( + &payer.pubkey(), + mint, + token_pool_bump, + is_token_22, + ); + instructions.push(instruction); + } + } + rpc.create_and_send_transaction(&instructions, &payer.pubkey(), &[payer]) + .await?; + Ok(created_token_pools) +} + /// Creates a spl token account and initializes it with the given mint and owner. /// This function is useful to create token accounts for spl compression and decompression tests. pub async fn create_token_account( @@ -571,6 +639,7 @@ pub async fn compressed_transfer_22_test>( delegate_change_account_index, None, token_22, + &[], ) .unwrap(); let sum_input_lamports = input_compressed_accounts @@ -664,13 +733,18 @@ pub async fn decompress_test>( recipient_token_account: &Pubkey, transaction_params: Option, is_token_22: bool, + token_pool_bump: u8, + additonal_pool_accounts: Option>, ) { let max_amount: u64 = input_compressed_accounts .iter() .map(|x| x.token_data.amount) .sum(); + println!("max_amount: {}", max_amount); + println!("amount: {}", amount); + let output_amount = max_amount - amount; let change_out_compressed_account = TokenTransferOutputData { - amount: max_amount - amount, + amount: output_amount, owner: payer.pubkey(), lamports: None, merkle_tree: *output_merkle_tree_pubkey, @@ -693,6 +767,8 @@ pub async fn decompress_test>( ) .await; let mint = input_compressed_accounts[0].token_data.mint; + let token_pool_pda = get_token_pool_pda_with_bump(&mint, token_pool_bump); + let instruction = create_transfer_instruction( &rpc.get_payer().pubkey(), &payer.pubkey(), // authority @@ -713,16 +789,20 @@ pub async fn decompress_test>( .map(|x| &x.compressed_account.compressed_account) .cloned() .collect::>(), - mint, // mint - None, // owner_if_delegate_change_account_index - false, // is_compress - Some(amount), // compression_amount - Some(get_token_pool_pda(&mint)), // token_pool_pda - Some(*recipient_token_account), // compress_or_decompress_token_account + mint, // mint + None, // owner_if_delegate_change_account_index + false, // is_compress + Some(amount), // compression_amount + Some(token_pool_pda), // token_pool_pda + Some(*recipient_token_account), // compress_or_decompress_token_account true, None, None, is_token_22, + additonal_pool_accounts + .clone() + .unwrap_or_default() + .as_slice(), ) .unwrap(); let output_merkle_tree_pubkeys = vec![*output_merkle_tree_pubkey]; @@ -742,6 +822,30 @@ pub async fn decompress_test>( .data, ) .unwrap(); + let mut token_pool_pre_balances = vec![ + spl_token::state::Account::unpack( + &rpc.get_account(token_pool_pda).await.unwrap().unwrap().data, + ) + .unwrap() + .amount, + ]; + for additional_pool_account in additonal_pool_accounts + .clone() + .unwrap_or_default() + .as_slice() + { + token_pool_pre_balances.push( + spl_token::state::Account::unpack( + &rpc.get_account(*additional_pool_account) + .await + .unwrap() + .unwrap() + .data, + ) + .unwrap() + .amount, + ); + } let context_payer = rpc.get_payer().insecure_clone(); let (event, _signature, _) = rpc .create_and_send_transaction_with_event::( @@ -772,7 +876,6 @@ pub async fn decompress_test>( None, ) .await; - let recipient_token_account_data = spl_token::state::Account::unpack( &rpc.get_account(*recipient_token_account) .await @@ -781,10 +884,44 @@ pub async fn decompress_test>( .data, ) .unwrap(); + println!("amount: {}", amount); assert_eq!( recipient_token_account_data.amount, recipient_token_account_data_pre.amount + amount ); + let token_pool_post_balance = spl_token::state::Account::unpack( + &rpc.get_account(token_pool_pda).await.unwrap().unwrap().data, + ) + .unwrap() + .amount; + assert_eq!( + token_pool_post_balance, + token_pool_pre_balances[0].saturating_sub(amount) + ); + let mut amount = amount - token_pool_pre_balances[0]; + for (i, additional_account) in additonal_pool_accounts + .unwrap_or_default() + .iter() + .enumerate() + { + let post_balance = spl_token::state::Account::unpack( + &rpc.get_account(*additional_account) + .await + .unwrap() + .unwrap() + .data, + ) + .unwrap() + .amount; + amount -= token_pool_pre_balances[i + 1]; + if amount == 0 { + break; + } + assert_eq!( + post_balance, + token_pool_pre_balances[i + 1].saturating_sub(amount) + ); + } } #[allow(clippy::too_many_arguments)] @@ -798,6 +935,7 @@ pub async fn perform_compress_spl_token_account> merkle_tree_pubkey: &Pubkey, remaining_amount: Option, is_token_22: bool, + token_pool_bump: u8, ) -> Result<(), RpcError> { let pre_token_account_amount = spl_token::state::Account::unpack( &rpc.get_account(*token_account).await.unwrap().unwrap().data, @@ -814,6 +952,7 @@ pub async fn perform_compress_spl_token_account> merkle_tree_pubkey, token_account, is_token_22, + token_pool_bump, ); let (event, _, _) = rpc .create_and_send_transaction_with_event::( @@ -870,6 +1009,8 @@ pub async fn compress_test>( sender_token_account: &Pubkey, transaction_params: Option, is_token_22: bool, + token_pool_bump: u8, + additonal_pool_accounts: Option>, ) { let output_compressed_account = TokenTransferOutputData { amount, @@ -885,18 +1026,19 @@ pub async fn compress_test>( &[output_compressed_account], // output_compressed_accounts &Vec::new(), // root_indices &None, - &Vec::new(), // input_token_data - &Vec::new(), // input_compressed_accounts - *mint, // mint - None, // owner_if_delegate_is_signer - true, // is_compress - Some(amount), // compression_amount - Some(get_token_pool_pda(mint)), // token_pool_pda - Some(*sender_token_account), // compress_or_decompress_token_account + &Vec::new(), // input_token_data + &Vec::new(), // input_compressed_accounts + *mint, // mint + None, // owner_if_delegate_is_signer + true, // is_compress + Some(amount), // compression_amount + Some(get_token_pool_pda_with_bump(mint, token_pool_bump)), // token_pool_pda + Some(*sender_token_account), // compress_or_decompress_token_account true, None, None, is_token_22, + additonal_pool_accounts.unwrap_or_default().as_slice(), ) .unwrap(); let output_merkle_tree_pubkeys = vec![*output_merkle_tree_pubkey]; @@ -1427,6 +1569,7 @@ pub async fn burn_test>( signer_is_delegate: bool, transaction_params: Option, is_token_22: bool, + token_pool_bump: u8, ) { let ( input_compressed_account_hashes, @@ -1444,6 +1587,8 @@ pub async fn burn_test>( signer_is_delegate, BurnInstructionMode::Normal, is_token_22, + token_pool_bump, + None, ) .await; let output_merkle_tree_pubkeys = vec![*change_account_merkle_tree; 1]; @@ -1456,7 +1601,7 @@ pub async fn burn_test>( Vec::new() }; - let token_pool_pda_address = get_token_pool_pda(&mint); + let token_pool_pda_address = get_token_pool_pda_with_bump(&mint, token_pool_bump); let pre_token_pool_account = rpc .get_account(token_pool_pda_address) .await @@ -1567,6 +1712,8 @@ pub async fn create_burn_test_instruction>( signer_is_delegate: bool, mode: BurnInstructionMode, is_token_22: bool, + token_pool_bump: u8, + additonal_pool_accounts: Option>, ) -> (Vec<[u8; 32]>, Vec, Pubkey, u64, Instruction) { let input_compressed_account_hashes = input_compressed_accounts .iter() @@ -1622,6 +1769,8 @@ pub async fn create_burn_test_instruction>( signer_is_delegate, burn_amount, is_token_22, + token_pool_bump, + additonal_pool_accounts: additonal_pool_accounts.unwrap_or_default(), }; let input_amount_sum = input_compressed_accounts .iter()