diff --git a/Cargo.lock b/Cargo.lock index af5725380a..4412efe2ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4365,6 +4365,8 @@ dependencies = [ "light-hasher", "light-macros", "light-token-interface", + "rand 0.8.5", + "solana-account-info", "solana-msg 2.2.1", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", diff --git a/js/compressed-token/src/v3/actions/create-mint-interface.ts b/js/compressed-token/src/v3/actions/create-mint-interface.ts index 757c38a6b7..f57f7bbd40 100644 --- a/js/compressed-token/src/v3/actions/create-mint-interface.ts +++ b/js/compressed-token/src/v3/actions/create-mint-interface.ts @@ -81,9 +81,7 @@ export async function createMintInterface( // Default: light-token mint creation if (!('secretKey' in mintAuthority)) { - throw new Error( - 'mintAuthority must be a Signer for light-token mints', - ); + throw new Error('mintAuthority must be a Signer for light-token mints'); } if ( addressTreeInfo && diff --git a/js/compressed-token/src/v3/get-mint-interface.ts b/js/compressed-token/src/v3/get-mint-interface.ts index aeffe71611..3d5bf10033 100644 --- a/js/compressed-token/src/v3/get-mint-interface.ts +++ b/js/compressed-token/src/v3/get-mint-interface.ts @@ -103,9 +103,7 @@ export async function getMintInterface( ); if (!compressedAccount?.data?.data) { - throw new Error( - `Light mint not found for ${address.toString()}`, - ); + throw new Error(`Light mint not found for ${address.toString()}`); } const compressedData = Buffer.from(compressedAccount.data.data); diff --git a/sdk-libs/sdk-types/Cargo.toml b/sdk-libs/sdk-types/Cargo.toml index 32a4c0da33..bf3ee4da20 100644 --- a/sdk-libs/sdk-types/Cargo.toml +++ b/sdk-libs/sdk-types/Cargo.toml @@ -39,6 +39,10 @@ thiserror = { workspace = true } [dev-dependencies] solana-pubkey = { workspace = true } +solana-account-info = { workspace = true } +light-account-checks = { workspace = true, features = ["solana", "test-only"] } +light-compressed-account = { workspace = true, features = ["keccak"] } +rand = { workspace = true } [lints.rust.unexpected_cfgs] level = "allow" diff --git a/sdk-libs/sdk-types/src/interface/program/compression/processor.rs b/sdk-libs/sdk-types/src/interface/program/compression/processor.rs index 20018c1de3..0597b2e01f 100644 --- a/sdk-libs/sdk-types/src/interface/program/compression/processor.rs +++ b/sdk-libs/sdk-types/src/interface/program/compression/processor.rs @@ -58,22 +58,22 @@ pub type CompressDispatchFn = fn( ctx: &mut CompressCtx<'_, AI>, ) -> Result<(), LightSdkTypesError>; -/// Process compress-and-close for PDA accounts (idempotent). -/// -/// Iterates over PDA accounts, dispatches each for compression via `dispatch_fn`, -/// then invokes the Light system program CPI to commit compressed state, -/// and closes the PDA accounts (transferring lamports to rent_sponsor). -/// -/// Idempotent: if any account is not yet compressible (rent function check fails), -/// the entire batch is silently skipped. -#[inline(never)] -pub fn process_compress_pda_accounts_idempotent( - remaining_accounts: &[AI], +/// Result of building CPI data for compress-and-close. +pub struct CompressPdaBuilt<'a, AI: AccountInfoTrait + Clone> { + pub cpi_ix_data: InstructionDataInvokeCpiWithAccountInfo, + pub cpi_accounts: CpiAccounts<'a, AI>, + pub pda_indices_to_close: Vec, +} + +/// Validates accounts and builds CPI data for compress-and-close. +/// Returns None when any account is non-compressible (idempotent skip, no CPI needed). +pub fn build_compress_pda_cpi_data<'a, AI: AccountInfoTrait + Clone>( + remaining_accounts: &'a [AI], params: &CompressAndCloseParams, dispatch_fn: CompressDispatchFn, cpi_signer: CpiSigner, program_id: &[u8; 32], -) -> Result<(), LightSdkTypesError> { +) -> Result>, LightSdkTypesError> { let system_accounts_offset = params.system_accounts_offset as usize; let num_pdas = params.compressed_accounts.len(); @@ -81,6 +81,10 @@ pub fn process_compress_pda_accounts_idempotent( return Err(LightSdkTypesError::InvalidInstructionData); } + if system_accounts_offset > remaining_accounts.len() { + return Err(LightSdkTypesError::InvalidInstructionData); + } + // 2. Load and validate config let config = LightConfig::load_checked(&remaining_accounts[CONFIG_INDEX], program_id)?; @@ -120,7 +124,7 @@ pub fn process_compress_pda_accounts_idempotent( // 6. Idempotent: if any account is not yet compressible, skip entire batch if has_non_compressible { - return Ok(()); + return Ok(None); } // 7. Build CPI instruction data @@ -138,14 +142,42 @@ pub fn process_compress_pda_accounts_idempotent( cpi_signer, ); - // 9. Invoke Light system program CPI - cpi_ix_data.invoke::(cpi_accounts)?; + Ok(Some(CompressPdaBuilt { + cpi_ix_data, + cpi_accounts, + pda_indices_to_close, + })) +} - // 10. Close PDA accounts, transferring lamports to rent_sponsor - for pda_index in &pda_indices_to_close { - light_account_checks::close_account(&remaining_accounts[*pda_index], rent_sponsor) - .map_err(LightSdkTypesError::AccountError)?; +/// Process compress-and-close for PDA accounts (idempotent). +/// +/// Iterates over PDA accounts, dispatches each for compression via `dispatch_fn`, +/// then invokes the Light system program CPI to commit compressed state, +/// and closes the PDA accounts (transferring lamports to rent_sponsor). +/// +/// Idempotent: if any account is not yet compressible (rent function check fails), +/// the entire batch is silently skipped. +#[inline(never)] +pub fn process_compress_pda_accounts_idempotent( + remaining_accounts: &[AI], + params: &CompressAndCloseParams, + dispatch_fn: CompressDispatchFn, + cpi_signer: CpiSigner, + program_id: &[u8; 32], +) -> Result<(), LightSdkTypesError> { + if let Some(built) = build_compress_pda_cpi_data( + remaining_accounts, + params, + dispatch_fn, + cpi_signer, + program_id, + )? { + let rent_sponsor = &remaining_accounts[RENT_SPONSOR_INDEX]; + built.cpi_ix_data.invoke::(built.cpi_accounts)?; + for pda_index in &built.pda_indices_to_close { + light_account_checks::close_account(&remaining_accounts[*pda_index], rent_sponsor) + .map_err(LightSdkTypesError::AccountError)?; + } } - Ok(()) } diff --git a/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs b/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs index 66cc8eb141..2fc7cfc811 100644 --- a/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs +++ b/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs @@ -133,27 +133,21 @@ pub struct DecompressCtx<'a, AI: AccountInfoTrait + Clone> { // PDA-only Processor // ============================================================================ -/// Process decompression for PDA accounts (idempotent, PDA-only). -/// -/// Iterates over PDA accounts, dispatches each for decompression via `DecompressVariant`, -/// then invokes the Light system program CPI to commit compressed state. -/// -/// Idempotent: if a PDA is already initialized, it is silently skipped. -/// -/// # Account layout in remaining_accounts: -/// - `[0]`: fee_payer (Signer, mut) -/// - `[1]`: config (LightConfig PDA) -/// - `[2]`: rent_sponsor (mut) -/// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts -/// - `[hot_accounts_start..]`: PDA accounts to decompress into -#[inline(never)] -pub fn process_decompress_pda_accounts_idempotent( - remaining_accounts: &[AI], +/// Result of building CPI data for PDA decompression. +pub struct DecompressPdaBuilt<'a, AI: AccountInfoTrait + Clone> { + pub cpi_ix_data: InstructionDataInvokeCpiWithAccountInfo, + pub cpi_accounts: CpiAccounts<'a, AI>, +} + +/// Validates accounts and builds CPI data for PDA decompression. +/// Returns None when all accounts already initialized (idempotent skip, no CPI needed). +pub fn build_decompress_pda_cpi_data<'a, AI, V>( + remaining_accounts: &'a [AI], params: &DecompressIdempotentParams, cpi_signer: CpiSigner, program_id: &[u8; 32], current_slot: u64, -) -> Result<(), LightSdkTypesError> +) -> Result>, LightSdkTypesError> where AI: AccountInfoTrait + Clone, V: DecompressVariant, @@ -234,51 +228,90 @@ where // 6. If no compressed accounts were produced (all already initialized), skip CPI if compressed_account_infos.is_empty() { - return Ok(()); + return Ok(None); } - // 7. Build and invoke Light system program CPI + // 7. Build CPI instruction data let mut cpi_ix_data = InstructionDataInvokeCpiWithAccountInfo::new( program_id.into(), cpi_signer.bump, params.proof.into(), ); cpi_ix_data.account_infos = compressed_account_infos; - cpi_ix_data.invoke::(cpi_accounts)?; - Ok(()) + Ok(Some(DecompressPdaBuilt { + cpi_ix_data, + cpi_accounts, + })) } -// ============================================================================ -// Full Processor (PDA + Token) -// ============================================================================ - -/// Process decompression for both PDA and token accounts (idempotent). +/// Process decompression for PDA accounts (idempotent, PDA-only). /// -/// Handles the combined PDA + token decompression flow: -/// - PDA accounts are decompressed first -/// - If both PDAs and tokens exist, PDA data is written to CPI context first -/// - Token accounts are decompressed via Transfer2 CPI to the light token program +/// Iterates over PDA accounts, dispatches each for decompression via `DecompressVariant`, +/// then invokes the Light system program CPI to commit compressed state. +/// +/// Idempotent: if a PDA is already initialized, it is silently skipped. /// /// # Account layout in remaining_accounts: /// - `[0]`: fee_payer (Signer, mut) /// - `[1]`: config (LightConfig PDA) /// - `[2]`: rent_sponsor (mut) -/// - `[3]`: ctoken_rent_sponsor (mut) -/// - `[4]`: light_token_program -/// - `[5]`: cpi_authority -/// - `[6]`: ctoken_compressible_config /// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts -/// - `[hot_accounts_start..]`: Hot accounts (PDAs then tokens) -#[cfg(feature = "token")] +/// - `[hot_accounts_start..]`: PDA accounts to decompress into #[inline(never)] -pub fn process_decompress_accounts_idempotent( +pub fn process_decompress_pda_accounts_idempotent( remaining_accounts: &[AI], params: &DecompressIdempotentParams, cpi_signer: CpiSigner, program_id: &[u8; 32], current_slot: u64, ) -> Result<(), LightSdkTypesError> +where + AI: AccountInfoTrait + Clone, + V: DecompressVariant, +{ + if let Some(built) = build_decompress_pda_cpi_data( + remaining_accounts, + params, + cpi_signer, + program_id, + current_slot, + )? { + built.cpi_ix_data.invoke::(built.cpi_accounts)?; + } + Ok(()) +} + +// ============================================================================ +// Full Processor (PDA + Token) +// ============================================================================ + +/// Result of building CPI data for the combined PDA + token decompression. +#[cfg(feature = "token")] +pub struct DecompressAccountsBuilt<'a, AI: AccountInfoTrait + Clone> { + pub cpi_accounts: CpiAccounts<'a, AI>, + pub compressed_account_infos: Vec, + pub has_pda_accounts: bool, + pub has_token_accounts: bool, + pub cpi_context: bool, + pub in_token_data: Vec, + pub in_tlv: Option>>, + pub token_seeds: Vec>, +} + +/// Validates accounts, dispatches all variants, and collects CPI inputs for +/// the combined PDA + token decompression. +/// +/// Returns the assembled [`DecompressAccountsBuilt`] on success. +/// The caller is responsible for executing the actual CPIs. +#[cfg(feature = "token")] +pub fn build_decompress_accounts_cpi_data<'a, AI, V>( + remaining_accounts: &'a [AI], + params: &DecompressIdempotentParams, + cpi_signer: CpiSigner, + program_id: &[u8; 32], + current_slot: u64, +) -> Result, LightSdkTypesError> where AI: AccountInfoTrait + Clone, V: DecompressVariant, @@ -388,6 +421,66 @@ where ) }; + Ok(DecompressAccountsBuilt { + cpi_accounts, + compressed_account_infos, + has_pda_accounts, + has_token_accounts, + cpi_context, + in_token_data, + in_tlv, + token_seeds, + }) +} + +/// Process decompression for both PDA and token accounts (idempotent). +/// +/// Handles the combined PDA + token decompression flow: +/// - PDA accounts are decompressed first +/// - If both PDAs and tokens exist, PDA data is written to CPI context first +/// - Token accounts are decompressed via Transfer2 CPI to the light token program +/// +/// # Account layout in remaining_accounts: +/// - `[0]`: fee_payer (Signer, mut) +/// - `[1]`: config (LightConfig PDA) +/// - `[2]`: rent_sponsor (mut) +/// - `[3]`: ctoken_rent_sponsor (mut) +/// - `[4]`: light_token_program +/// - `[5]`: cpi_authority +/// - `[6]`: ctoken_compressible_config +/// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts +/// - `[hot_accounts_start..]`: Hot accounts (PDAs then tokens) +#[cfg(feature = "token")] +#[inline(never)] +pub fn process_decompress_accounts_idempotent( + remaining_accounts: &[AI], + params: &DecompressIdempotentParams, + cpi_signer: CpiSigner, + program_id: &[u8; 32], + current_slot: u64, +) -> Result<(), LightSdkTypesError> +where + AI: AccountInfoTrait + Clone, + V: DecompressVariant, +{ + let DecompressAccountsBuilt { + cpi_accounts, + compressed_account_infos, + has_pda_accounts, + has_token_accounts, + cpi_context, + in_token_data, + in_tlv, + token_seeds, + } = build_decompress_accounts_cpi_data( + remaining_accounts, + params, + cpi_signer, + program_id, + current_slot, + )?; + let system_accounts_offset = params.system_accounts_offset as usize; + // 7. PDA CPI (Light system program) if has_pda_accounts { let pda_only = !cpi_context; diff --git a/sdk-libs/sdk-types/tests/common/mod.rs b/sdk-libs/sdk-types/tests/common/mod.rs new file mode 100644 index 0000000000..6a41ea4a1d --- /dev/null +++ b/sdk-libs/sdk-types/tests/common/mod.rs @@ -0,0 +1,94 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_checks::{ + account_info::test_account_info::solana_program::TestAccount, discriminator::Discriminator, +}; +use light_compressible::rent::RentConfig; +use light_sdk_types::{ + error::LightSdkTypesError, + instruction::PackedStateTreeInfo, + interface::program::{ + config::{LightConfig, LIGHT_CONFIG_SEED, RENT_SPONSOR_SEED}, + decompression::processor::{DecompressCtx, DecompressVariant}, + }, +}; +use solana_account_info::AccountInfo; +use solana_pubkey::Pubkey; + +/// Creates a fully valid LightConfig TestAccount for the given program_id. +/// Returns (config_account, rent_sponsor_key). +pub fn make_config_account(program_id: [u8; 32]) -> (TestAccount, [u8; 32]) { + let prog = Pubkey::from(program_id); + + let config_bump_u16 = 0u16; + let (config_pda, bump) = + Pubkey::find_program_address(&[LIGHT_CONFIG_SEED, &config_bump_u16.to_le_bytes()], &prog); + let (rent_sponsor_pda, rent_sponsor_bump) = + Pubkey::find_program_address(&[RENT_SPONSOR_SEED], &prog); + + let config = LightConfig { + version: 1, + write_top_up: 1000, + update_authority: [1u8; 32], + rent_sponsor: rent_sponsor_pda.to_bytes(), + compression_authority: [2u8; 32], + rent_config: RentConfig::default(), + config_bump: 0, + bump, + rent_sponsor_bump, + address_space: vec![[3u8; 32]], + }; + + let mut data = LightConfig::LIGHT_DISCRIMINATOR.to_vec(); + config.serialize(&mut data).unwrap(); + + let mut account = TestAccount::new(config_pda, Pubkey::from(program_id), data.len()); + account.data = data; + + (account, rent_sponsor_pda.to_bytes()) +} + +/// Creates a dummy writable TestAccount with the given key, owner, and data size. +pub fn make_dummy_account(key: [u8; 32], owner: [u8; 32], size: usize) -> TestAccount { + TestAccount::new(Pubkey::from(key), Pubkey::from(owner), size) +} + +/// Standard 5-account layout used by compress and decompress tests. +/// [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system_account, [4]=pda_account +pub fn make_valid_accounts( + program_id: [u8; 32], +) -> ( + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, +) { + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let system_account = make_dummy_account([11u8; 32], [0u8; 32], 0); + let pda_account = make_dummy_account([10u8; 32], program_id, 100); + ( + fee_payer, + config_account, + rent_sponsor, + system_account, + pda_account, + ) +} + +/// Simulates an already-initialized PDA: pushes nothing to compressed_account_infos. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +#[allow(dead_code)] +pub struct SkipVariant; + +impl<'info> DecompressVariant> for SkipVariant { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + _ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + Ok(()) + } +} diff --git a/sdk-libs/sdk-types/tests/compress_processor.rs b/sdk-libs/sdk-types/tests/compress_processor.rs new file mode 100644 index 0000000000..b0a749ba84 --- /dev/null +++ b/sdk-libs/sdk-types/tests/compress_processor.rs @@ -0,0 +1,522 @@ +mod common; + +use common::{make_dummy_account, make_valid_accounts}; +use light_compressed_account::instruction_data::{ + compressed_proof::ValidityProof, + with_account_info::{CompressedAccountInfo, InstructionDataInvokeCpiWithAccountInfo}, +}; +use light_sdk_types::{ + error::LightSdkTypesError, + instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + interface::program::compression::processor::{ + build_compress_pda_cpi_data, CompressAndCloseParams, CompressCtx, + }, + CpiSigner, +}; +use solana_account_info::AccountInfo; +use solana_pubkey::Pubkey; + +// ============================================================================ +// Mock dispatch functions +// ============================================================================ + +fn mock_dispatch_compressible<'a>( + _account: &AccountInfo<'a>, + _meta: &CompressedAccountMetaNoLamportsNoAddress, + pda_index: usize, + ctx: &mut CompressCtx<'_, AccountInfo<'a>>, +) -> Result<(), LightSdkTypesError> { + ctx.compressed_account_infos.push(CompressedAccountInfo { + address: None, + input: None, + output: None, + }); + ctx.pda_indices_to_close.push(pda_index); + Ok(()) +} + +fn mock_dispatch_non_compressible<'a>( + _account: &AccountInfo<'a>, + _meta: &CompressedAccountMetaNoLamportsNoAddress, + _pda_index: usize, + ctx: &mut CompressCtx<'_, AccountInfo<'a>>, +) -> Result<(), LightSdkTypesError> { + ctx.has_non_compressible = true; + Ok(()) +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[test] +fn test_system_offset_exceeds_accounts_returns_error() { + let program_id = [42u8; 32]; + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut dummy2 = make_dummy_account([2u8; 32], [0u8; 32], 0); + let mut dummy3 = make_dummy_account([3u8; 32], [0u8; 32], 0); + + let fee_payer_ai = fee_payer.get_account_info(); + let dummy2_ai = dummy2.get_account_info(); + let dummy3_ai = dummy3.get_account_info(); + + // system_accounts_offset = 100 > remaining_accounts.len() = 3 -> error + let remaining_accounts = vec![fee_payer_ai, dummy2_ai, dummy3_ai]; + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 100, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); +} + +#[test] +fn test_empty_compressed_accounts_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); +} + +#[test] +fn test_not_enough_remaining_accounts_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, _, _) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + + // Only 3 accounts, but 10 compressed_accounts requested -> checked_sub underflows + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default(); 10], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::NotEnoughAccountKeys) + )); +} + +#[test] +fn test_config_wrong_owner_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + // Override config account owner to a wrong value + config_account.owner = Pubkey::from([99u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_config_wrong_discriminator_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + // Override config data with zeros (wrong discriminator) + config_account.data = vec![0u8; 170]; + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_wrong_rent_sponsor_key_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + // Override rent_sponsor key to a value that doesn't match config.rent_sponsor + rent_sponsor.key = Pubkey::from([77u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidRentSponsor) + )); +} + +#[test] +fn test_idempotent_returns_none_when_non_compressible() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_non_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Ok(None))); +} + +#[test] +fn test_build_compress_produces_expected_instruction_data() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + // [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system, [4]=pda + // num_pdas=1, pda_start = 5 - 1 = 4 -> pda_index = 4 + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + let expected_cpi_ix_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, + bump: 255, + invoking_program_id: program_id.into(), + account_infos: vec![CompressedAccountInfo { + address: None, + input: None, + output: None, + }], + proof: None, + ..Default::default() + }; + + let built = result.unwrap().unwrap(); + assert_eq!(built.cpi_ix_data, expected_cpi_ix_data); + // pda_start = 5 - 1 = 4 + assert_eq!(built.pda_indices_to_close, vec![4usize]); +} + +fn mock_dispatch_error<'a>( + _account: &AccountInfo<'a>, + _meta: &CompressedAccountMetaNoLamportsNoAddress, + _pda_index: usize, + _ctx: &mut CompressCtx<'_, AccountInfo<'a>>, +) -> Result<(), LightSdkTypesError> { + Err(LightSdkTypesError::ConstraintViolation) +} + +#[test] +fn test_dispatch_fn_error_propagates() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_error, + cpi_signer, + &program_id, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_multiple_pdas_non_compressible_skips_all() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda1) = + make_valid_accounts(program_id); + let mut pda2 = make_dummy_account([11u8; 32], program_id, 100); + let mut pda3 = make_dummy_account([12u8; 32], program_id, 100); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda1_ai = pda1.get_account_info(); + let pda2_ai = pda2.get_account_info(); + let pda3_ai = pda3.get_account_info(); + + // [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system, [4]=pda1, [5]=pda2, [6]=pda3 + // pda_start = 7 - 3 = 4; accounts[4],[5],[6] are the three PDAs + let remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + system_ai, + pda1_ai, + pda2_ai, + pda3_ai, + ]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![ + CompressedAccountMetaNoLamportsNoAddress::default(), + CompressedAccountMetaNoLamportsNoAddress::default(), + CompressedAccountMetaNoLamportsNoAddress::default(), + ], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_non_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Ok(None))); +} + +#[test] +fn test_empty_system_accounts_slice() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, _, _) = + make_valid_accounts(program_id); + let mut pda_account = make_dummy_account([10u8; 32], program_id, 100); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + // [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=pda + // system_accounts_offset=3, pda_start = 4 - 1 = 3 = system_accounts_offset + // remaining_accounts[3..3] = empty system slice -> no panic + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, pda_ai]; + + let params = CompressAndCloseParams { + proof: ValidityProof::default(), + compressed_accounts: vec![CompressedAccountMetaNoLamportsNoAddress::default()], + system_accounts_offset: 3, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_compress_pda_cpi_data( + &remaining_accounts, + ¶ms, + mock_dispatch_compressible, + cpi_signer, + &program_id, + ); + + assert!(matches!(result, Ok(Some(_)))); +} diff --git a/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs b/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs new file mode 100644 index 0000000000..3414f4c922 --- /dev/null +++ b/sdk-libs/sdk-types/tests/decompress_accounts_processor.rs @@ -0,0 +1,1025 @@ +#![cfg(feature = "token")] + +mod common; + +use borsh::{BorshDeserialize, BorshSerialize}; +use common::{make_config_account, make_dummy_account, make_valid_accounts, SkipVariant}; +use light_account_checks::account_info::test_account_info::solana_program::TestAccount; +use light_compressed_account::{ + compressed_account::PackedMerkleContext, + instruction_data::{compressed_proof::ValidityProof, with_account_info::CompressedAccountInfo}, +}; +use light_sdk_types::{ + error::LightSdkTypesError, + instruction::PackedStateTreeInfo, + interface::{ + account::compression_info::CompressedAccountData, + program::decompression::processor::{ + build_decompress_accounts_cpi_data, DecompressCtx, DecompressIdempotentParams, + DecompressVariant, + }, + }, + CpiSigner, +}; +use light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext; +use rand::{Rng, SeedableRng}; +use solana_account_info::AccountInfo; +use solana_pubkey::Pubkey; + +// ============================================================================ +// Mock DecompressVariant implementations +// ============================================================================ + +/// Carries and pushes a specific CompressedAccountInfo — simulates PDA decompression. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +struct PdaMockVariant(CompressedAccountInfo); + +impl<'info> DecompressVariant> for PdaMockVariant { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + ctx.compressed_account_infos.push(self.0.clone()); + Ok(()) + } +} + +/// Carries and pushes a specific MultiInputTokenDataWithContext — simulates token decompression. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +struct TokenMockVariant(MultiInputTokenDataWithContext); + +impl<'info> DecompressVariant> for TokenMockVariant { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + ctx.in_token_data.push(self.0); + Ok(()) + } +} + +/// Unified enum for tests that mix PDA and token accounts in one params.accounts Vec. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +enum MockVariant { + Pda(PdaMockVariant), + Token(TokenMockVariant), + Error, +} + +impl<'info> DecompressVariant> for MockVariant { + fn decompress( + &self, + meta: &PackedStateTreeInfo, + pda_account: &AccountInfo<'info>, + ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + match self { + MockVariant::Pda(v) => v.decompress(meta, pda_account, ctx), + MockVariant::Token(v) => v.decompress(meta, pda_account, ctx), + MockVariant::Error => Err(LightSdkTypesError::ConstraintViolation), + } + } +} + +// ============================================================================ +// Helpers +// ============================================================================ + +/// 9-account layout for PDA+token tests. +/// [0]=fee_payer, [1]=config, [2]=rent_sponsor, +/// [3]=ctoken_rent_sponsor, [4..6]=dummies, [6]=ctoken_compressible_config, +/// (system_accounts_offset=7, no system accounts between 7 and hot_accounts_start) +/// [7]=pda_account, [8]=token_account +fn make_valid_accounts_with_tokens( + program_id: [u8; 32], +) -> ( + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, + TestAccount, +) { + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let ctoken_rent_sponsor = make_dummy_account([3u8; 32], [0u8; 32], 0); + let dummy4 = make_dummy_account([4u8; 32], [0u8; 32], 0); + let dummy5 = make_dummy_account([5u8; 32], [0u8; 32], 0); + let ctoken_config = make_dummy_account([6u8; 32], [0u8; 32], 0); + let pda_account = make_dummy_account([10u8; 32], program_id, 100); + let token_account = make_dummy_account([20u8; 32], program_id, 100); + ( + fee_payer, + config_account, + rent_sponsor, + ctoken_rent_sponsor, + dummy4, + dummy5, + ctoken_config, + pda_account, + token_account, + ) +} + +fn one_pda_params( + data: V, + system_accounts_offset: u8, +) -> DecompressIdempotentParams { + DecompressIdempotentParams { + system_accounts_offset, + token_accounts_offset: 1, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data, + }], + } +} + +// ============================================================================ +// Error path tests +// ============================================================================ + +#[test] +fn test_system_offset_exceeds_accounts_returns_error() { + let program_id = [42u8; 32]; + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut dummy2 = make_dummy_account([2u8; 32], [0u8; 32], 0); + let mut dummy3 = make_dummy_account([3u8; 32], [0u8; 32], 0); + + let fee_payer_ai = fee_payer.get_account_info(); + let dummy2_ai = dummy2.get_account_info(); + let dummy3_ai = dummy3.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, dummy2_ai, dummy3_ai]; + let params = DecompressIdempotentParams { + system_accounts_offset: 100, + token_accounts_offset: 1, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); +} + +#[test] +fn test_bad_token_accounts_offset_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + // token_accounts_offset=99 > accounts.len()=1 -> split_at_checked returns None + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = DecompressIdempotentParams { + system_accounts_offset: 3, + token_accounts_offset: 99, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); +} + +#[test] +fn test_not_enough_hot_accounts_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, _, _) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + + // accounts.len()=5, remaining_accounts.len()=3 -> checked_sub(5) underflows + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai]; + let params = DecompressIdempotentParams { + system_accounts_offset: 3, + token_accounts_offset: 5, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![ + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }; + 5 + ], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::NotEnoughAccountKeys) + )); +} + +#[test] +fn test_config_wrong_owner_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + config_account.owner = Pubkey::from([99u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_wrong_rent_sponsor_key_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + rent_sponsor.key = Pubkey::from([77u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidRentSponsor) + )); +} + +// ============================================================================ +// Happy path tests +// ============================================================================ + +#[test] +fn test_pda_only_builds_correct_data() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, _, _) = + make_valid_accounts(program_id); + let mut pda_account = make_dummy_account([10u8; 32], program_id, 100); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + // [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=pda (hot) + // system_accounts_offset=3, token_accounts_offset=1 (=accounts.len() -> no tokens) + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, pda_ai]; + let params = one_pda_params( + PdaMockVariant(CompressedAccountInfo { + address: None, + input: None, + output: None, + }), + 3, + ); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + let built = result.unwrap(); + assert!(built.has_pda_accounts); + assert!(!built.has_token_accounts); + assert!(!built.cpi_context); + assert_eq!( + built.compressed_account_infos, + vec![CompressedAccountInfo { + address: None, + input: None, + output: None + }] + ); + assert_eq!( + built.in_token_data, + Vec::::new() + ); +} + +#[test] +fn test_token_only_builds_correct_data() { + let program_id = [42u8; 32]; + let ( + mut fee_payer, + mut config_account, + mut rent_sponsor, + mut ctoken_rent_sponsor, + mut dummy4, + mut dummy5, + mut ctoken_config, + _, + mut token_account, + ) = make_valid_accounts_with_tokens(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let ctoken_rs_ai = ctoken_rent_sponsor.get_account_info(); + let dummy4_ai = dummy4.get_account_info(); + let dummy5_ai = dummy5.get_account_info(); + let ctoken_config_ai = ctoken_config.get_account_info(); + let token_ai = token_account.get_account_info(); + + // [0..6] fixed, [7]=token (hot); system_accounts_offset=7, no system accounts + let remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + ctoken_rs_ai, + dummy4_ai, + dummy5_ai, + ctoken_config_ai, + token_ai, + ]; + // token_accounts_offset=0: all accounts are tokens, no PDAs + let params = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: 0, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: TokenMockVariant(MultiInputTokenDataWithContext::default()), + }], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + let built = result.unwrap(); + assert!(!built.has_pda_accounts); + assert!(built.has_token_accounts); + assert!(!built.cpi_context); + assert_eq!( + built.compressed_account_infos, + Vec::::new() + ); + assert_eq!( + built.in_token_data, + vec![MultiInputTokenDataWithContext::default()] + ); +} + +#[test] +fn test_pda_and_token_sets_cpi_context() { + let program_id = [42u8; 32]; + let ( + mut fee_payer, + mut config_account, + mut rent_sponsor, + mut ctoken_rent_sponsor, + mut dummy4, + mut dummy5, + mut ctoken_config, + mut pda_account, + mut token_account, + ) = make_valid_accounts_with_tokens(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let ctoken_rs_ai = ctoken_rent_sponsor.get_account_info(); + let dummy4_ai = dummy4.get_account_info(); + let dummy5_ai = dummy5.get_account_info(); + let ctoken_config_ai = ctoken_config.get_account_info(); + let pda_ai = pda_account.get_account_info(); + let token_ai = token_account.get_account_info(); + + // [0..6] fixed, [7]=pda, [8]=token (hot); system_accounts_offset=7 + let remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + ctoken_rs_ai, + dummy4_ai, + dummy5_ai, + ctoken_config_ai, + pda_ai, + token_ai, + ]; + // token_accounts_offset=1: accounts[0]=PDA, accounts[1]=token + let params = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: 1, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![ + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Pda(PdaMockVariant(CompressedAccountInfo { + address: None, + input: None, + output: None, + })), + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Token(TokenMockVariant( + MultiInputTokenDataWithContext::default(), + )), + }, + ], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + let built = result.unwrap(); + assert!(built.has_pda_accounts); + assert!(built.has_token_accounts); + assert!(built.cpi_context); + assert_eq!( + built.compressed_account_infos, + vec![CompressedAccountInfo { + address: None, + input: None, + output: None + }] + ); + assert_eq!( + built.in_token_data, + vec![MultiInputTokenDataWithContext::default()] + ); +} + +#[test] +fn test_randomized_pda_and_token_decompression() { + let program_id = [42u8; 32]; + + // Explicit edge case: zero PDAs, zero tokens. + { + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut config_account = config_account; + let mut rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let mut ctoken_rent_sponsor = make_dummy_account([3u8; 32], [0u8; 32], 0); + let mut dummy4 = make_dummy_account([4u8; 32], [0u8; 32], 0); + let mut dummy5 = make_dummy_account([5u8; 32], [0u8; 32], 0); + let mut ctoken_config = make_dummy_account([6u8; 32], [0u8; 32], 0); + + let remaining_accounts = vec![ + fee_payer.get_account_info(), + config_account.get_account_info(), + rent_sponsor.get_account_info(), + ctoken_rent_sponsor.get_account_info(), + dummy4.get_account_info(), + dummy5.get_account_info(), + ctoken_config.get_account_info(), + ]; + + let params: DecompressIdempotentParams = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: 0, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let built = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ) + .unwrap(); + + assert_eq!( + built.compressed_account_infos, + Vec::::new(), + "zero-zero case: expected empty pda infos" + ); + assert_eq!( + built.in_token_data, + Vec::::new(), + "zero-zero case: expected empty token data" + ); + assert!(!built.has_pda_accounts, "zero-zero: has_pda_accounts"); + assert!(!built.has_token_accounts, "zero-zero: has_token_accounts"); + assert!(!built.cpi_context, "zero-zero: cpi_context"); + } + + for _ in 0..50 { + let seed: [u8; 32] = rand::thread_rng().gen(); + println!("seed: {seed:?}"); + let mut rng = rand::rngs::StdRng::from_seed(seed); + + // Pick random counts in 0..=5 (0,0 is valid; covered by explicit case above). + let n_pdas: usize = rng.gen_range(0..=5); + let n_tokens: usize = rng.gen_range(0..=5); + + // Build expected PDA infos with random addresses. + let expected_pda_infos: Vec = (0..n_pdas) + .map(|_| CompressedAccountInfo { + address: Some(rng.gen::<[u8; 32]>()), + input: None, + output: None, + }) + .collect(); + + // Build expected token data with random fields. + let expected_token_data: Vec = (0..n_tokens) + .map(|_| MultiInputTokenDataWithContext { + owner: rng.gen::(), + amount: rng.gen(), + has_delegate: rng.gen(), + delegate: rng.gen::(), + mint: rng.gen::(), + version: rng.gen::(), + root_index: rng.gen(), + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: rng.gen(), + queue_pubkey_index: rng.gen(), + leaf_index: rng.gen(), + prove_by_index: rng.gen(), + }, + }) + .collect(); + + // Build accounts: 7-account header + n_pdas PDAs + n_tokens token accounts. + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut config_account = config_account; + let mut rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let mut ctoken_rent_sponsor = make_dummy_account([3u8; 32], [0u8; 32], 0); + let mut dummy4 = make_dummy_account([4u8; 32], [0u8; 32], 0); + let mut dummy5 = make_dummy_account([5u8; 32], [0u8; 32], 0); + let mut ctoken_config = make_dummy_account([6u8; 32], [0u8; 32], 0); + let mut pda_accounts: Vec = (0..n_pdas) + .map(|i| make_dummy_account([(10 + i) as u8; 32], program_id, 100)) + .collect(); + let mut token_accounts: Vec = (0..n_tokens) + .map(|i| make_dummy_account([(20 + i) as u8; 32], program_id, 100)) + .collect(); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let ctoken_rs_ai = ctoken_rent_sponsor.get_account_info(); + let dummy4_ai = dummy4.get_account_info(); + let dummy5_ai = dummy5.get_account_info(); + let ctoken_config_ai = ctoken_config.get_account_info(); + let mut pda_ais: Vec> = pda_accounts + .iter_mut() + .map(|a| a.get_account_info()) + .collect(); + let mut token_ais: Vec> = token_accounts + .iter_mut() + .map(|a| a.get_account_info()) + .collect(); + + let mut remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + ctoken_rs_ai, + dummy4_ai, + dummy5_ai, + ctoken_config_ai, + ]; + remaining_accounts.append(&mut pda_ais); + remaining_accounts.append(&mut token_ais); + + // Build params.accounts: PDAs first, then tokens. + let mut accounts: Vec> = Vec::new(); + for info in &expected_pda_infos { + accounts.push(CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Pda(PdaMockVariant(info.clone())), + }); + } + for token in &expected_token_data { + accounts.push(CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Token(TokenMockVariant(*token)), + }); + } + + let params = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: n_pdas as u8, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts, + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + let built = result.unwrap(); + assert_eq!( + built.compressed_account_infos, expected_pda_infos, + "seed={seed:?} n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.in_token_data, expected_token_data, + "seed={seed:?} n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.has_pda_accounts, + n_pdas > 0, + "seed={seed:?} n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.has_token_accounts, + n_tokens > 0, + "seed={seed:?} n_pdas={n_pdas} n_tokens={n_tokens}" + ); + assert_eq!( + built.cpi_context, + n_pdas > 0 && n_tokens > 0, + "seed={seed:?} n_pdas={n_pdas} n_tokens={n_tokens}" + ); + } +} + +#[test] +fn test_config_wrong_discriminator_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + config_account.data = vec![0u8; 170]; + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_decompress_variant_error_during_pda_phase() { + let program_id = [42u8; 32]; + let ( + mut fee_payer, + mut config_account, + mut rent_sponsor, + mut ctoken_rent_sponsor, + mut dummy4, + mut dummy5, + mut ctoken_config, + mut pda_account, + mut token_account, + ) = make_valid_accounts_with_tokens(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let ctoken_rs_ai = ctoken_rent_sponsor.get_account_info(); + let dummy4_ai = dummy4.get_account_info(); + let dummy5_ai = dummy5.get_account_info(); + let ctoken_config_ai = ctoken_config.get_account_info(); + let pda_ai = pda_account.get_account_info(); + let token_ai = token_account.get_account_info(); + + let remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + ctoken_rs_ai, + dummy4_ai, + dummy5_ai, + ctoken_config_ai, + pda_ai, + token_ai, + ]; + + // First entry is Error (PDA phase) → error propagated; token phase never reached. + let params = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: 1, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![ + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Error, + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Token(TokenMockVariant( + MultiInputTokenDataWithContext::default(), + )), + }, + ], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_decompress_variant_error_during_token_phase() { + let program_id = [42u8; 32]; + let ( + mut fee_payer, + mut config_account, + mut rent_sponsor, + mut ctoken_rent_sponsor, + mut dummy4, + mut dummy5, + mut ctoken_config, + mut pda_account, + mut token_account, + ) = make_valid_accounts_with_tokens(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let ctoken_rs_ai = ctoken_rent_sponsor.get_account_info(); + let dummy4_ai = dummy4.get_account_info(); + let dummy5_ai = dummy5.get_account_info(); + let ctoken_config_ai = ctoken_config.get_account_info(); + let pda_ai = pda_account.get_account_info(); + let token_ai = token_account.get_account_info(); + + let remaining_accounts = vec![ + fee_payer_ai, + config_ai, + rent_sponsor_ai, + ctoken_rs_ai, + dummy4_ai, + dummy5_ai, + ctoken_config_ai, + pda_ai, + token_ai, + ]; + + // First entry is Pda (succeeds), second is Error (token phase) → error propagated. + let params = DecompressIdempotentParams { + system_accounts_offset: 7, + token_accounts_offset: 1, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![ + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Pda(PdaMockVariant(CompressedAccountInfo { + address: None, + input: None, + output: None, + })), + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Error, + }, + ], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_token_decompression_missing_ctoken_config() { + let program_id = [42u8; 32]; + let (config_account, rent_sponsor_key) = make_config_account(program_id); + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut config_account = config_account; + let mut rent_sponsor = make_dummy_account(rent_sponsor_key, [0u8; 32], 0); + let mut ctoken_rent_sponsor = make_dummy_account([3u8; 32], [0u8; 32], 0); + let mut hot_token = make_dummy_account([20u8; 32], program_id, 100); + + // Only 5 accounts: [0]=fee_payer, [1]=config, [2]=rent_sponsor, + // [3]=ctoken_rent_sponsor, [4]=hot_token. + // has_token_accounts = true triggers get(6) which returns None → NotEnoughAccountKeys. + let remaining_accounts = vec![ + fee_payer.get_account_info(), + config_account.get_account_info(), + rent_sponsor.get_account_info(), + ctoken_rent_sponsor.get_account_info(), + hot_token.get_account_info(), + ]; + + let params: DecompressIdempotentParams = DecompressIdempotentParams { + system_accounts_offset: 3, + token_accounts_offset: 0, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: MockVariant::Token(TokenMockVariant(MultiInputTokenDataWithContext::default())), + }], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = build_decompress_accounts_cpi_data( + &remaining_accounts, + ¶ms, + cpi_signer, + &program_id, + 0, + ); + + assert!(matches!( + result, + Err(LightSdkTypesError::NotEnoughAccountKeys) + )); +} diff --git a/sdk-libs/sdk-types/tests/decompress_processor.rs b/sdk-libs/sdk-types/tests/decompress_processor.rs new file mode 100644 index 0000000000..51dd6ac67d --- /dev/null +++ b/sdk-libs/sdk-types/tests/decompress_processor.rs @@ -0,0 +1,405 @@ +mod common; + +use borsh::{BorshDeserialize, BorshSerialize}; +use common::{make_dummy_account, make_valid_accounts, SkipVariant}; +use light_compressed_account::instruction_data::{ + compressed_proof::ValidityProof, + with_account_info::{CompressedAccountInfo, InstructionDataInvokeCpiWithAccountInfo}, +}; +use light_sdk_types::{ + error::LightSdkTypesError, + instruction::PackedStateTreeInfo, + interface::{ + account::compression_info::CompressedAccountData, + program::decompression::processor::{ + build_decompress_pda_cpi_data, DecompressCtx, DecompressIdempotentParams, + DecompressVariant, + }, + }, + CpiSigner, +}; +use solana_account_info::AccountInfo; +use solana_pubkey::Pubkey; + +// ============================================================================ +// Mock DecompressVariant implementations +// ============================================================================ + +/// Pushes one known CompressedAccountInfo to simulate decompression. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +struct DecompressVariantMock; + +impl<'info> DecompressVariant> for DecompressVariantMock { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + ctx.compressed_account_infos.push(CompressedAccountInfo { + address: None, + input: None, + output: None, + }); + Ok(()) + } +} + +fn one_pda_params( + data: V, + system_accounts_offset: u8, +) -> DecompressIdempotentParams { + DecompressIdempotentParams { + system_accounts_offset, + token_accounts_offset: 1, // 1 PDA account, 0 token accounts + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data, + }], + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[test] +fn test_system_offset_exceeds_accounts_returns_error() { + let program_id = [42u8; 32]; + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut dummy2 = make_dummy_account([2u8; 32], [0u8; 32], 0); + let mut dummy3 = make_dummy_account([3u8; 32], [0u8; 32], 0); + + let fee_payer_ai = fee_payer.get_account_info(); + let dummy2_ai = dummy2.get_account_info(); + let dummy3_ai = dummy3.get_account_info(); + + // system_accounts_offset = 100 > remaining_accounts.len() = 3 -> error + let remaining_accounts = vec![fee_payer_ai, dummy2_ai, dummy3_ai]; + let params = DecompressIdempotentParams { + system_accounts_offset: 100, + token_accounts_offset: 1, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); +} + +#[test] +fn test_empty_pda_accounts_returns_error() { + let program_id = [42u8; 32]; + let mut fee_payer = make_dummy_account([1u8; 32], [0u8; 32], 0); + let mut dummy2 = make_dummy_account([2u8; 32], [0u8; 32], 0); + let mut dummy3 = make_dummy_account([3u8; 32], [0u8; 32], 0); + + let fee_payer_ai = fee_payer.get_account_info(); + let dummy2_ai = dummy2.get_account_info(); + let dummy3_ai = dummy3.get_account_info(); + + // token_accounts_offset = 0 -> pda_accounts = accounts[0..0] = [] -> empty -> error + let remaining_accounts = vec![fee_payer_ai, dummy2_ai, dummy3_ai]; + let params = DecompressIdempotentParams { + system_accounts_offset: 3, + token_accounts_offset: 0, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidInstructionData) + )); +} + +#[test] +fn test_not_enough_hot_accounts_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, _, _) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + + // Only 3 remaining_accounts, 5 accounts in params -> checked_sub(5) on len=3 fails + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai]; + + let params = DecompressIdempotentParams { + system_accounts_offset: 3, + token_accounts_offset: 5, + output_queue_index: 0, + proof: ValidityProof::default(), + accounts: vec![ + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }, + CompressedAccountData { + tree_info: PackedStateTreeInfo::default(), + data: SkipVariant, + }, + ], + }; + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!( + result, + Err(LightSdkTypesError::NotEnoughAccountKeys) + )); +} + +#[test] +fn test_config_wrong_owner_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + // Override config account owner to a wrong value + config_account.owner = Pubkey::from([99u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_wrong_rent_sponsor_key_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + // Override rent_sponsor key to a value that doesn't match config.rent_sponsor + rent_sponsor.key = Pubkey::from([77u8; 32]); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!( + result, + Err(LightSdkTypesError::InvalidRentSponsor) + )); +} + +#[test] +fn test_idempotent_returns_none_when_all_initialized() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + // SkipVariant pushes nothing -> compressed_account_infos.is_empty() -> Ok(None) + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!(result, Ok(None))); +} + +#[test] +fn test_build_decompress_produces_expected_instruction_data() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + // [0]=fee_payer, [1]=config, [2]=rent_sponsor, [3]=system, [4]=pda + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + // DecompressVariantMock pushes one CompressedAccountInfo -> CPI data built + let params = one_pda_params(DecompressVariantMock, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + let expected_cpi_ix_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, + bump: 255, + invoking_program_id: program_id.into(), + account_infos: vec![CompressedAccountInfo { + address: None, + input: None, + output: None, + }], + proof: None, + ..Default::default() + }; + + let built = result.unwrap().unwrap(); + assert_eq!(built.cpi_ix_data, expected_cpi_ix_data); +} + +#[derive(BorshSerialize, BorshDeserialize, Clone)] +struct ErrorVariant; + +impl<'info> DecompressVariant> for ErrorVariant { + fn decompress( + &self, + _meta: &PackedStateTreeInfo, + _pda_account: &AccountInfo<'info>, + _ctx: &mut DecompressCtx<'_, AccountInfo<'info>>, + ) -> Result<(), LightSdkTypesError> { + Err(LightSdkTypesError::ConstraintViolation) + } +} + +#[test] +fn test_decompress_variant_error_propagates() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(ErrorVariant, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +} + +#[test] +fn test_config_wrong_discriminator_returns_error() { + let program_id = [42u8; 32]; + let (mut fee_payer, mut config_account, mut rent_sponsor, mut system_account, mut pda_account) = + make_valid_accounts(program_id); + + config_account.data = vec![0u8; 170]; + + let fee_payer_ai = fee_payer.get_account_info(); + let config_ai = config_account.get_account_info(); + let rent_sponsor_ai = rent_sponsor.get_account_info(); + let system_ai = system_account.get_account_info(); + let pda_ai = pda_account.get_account_info(); + + let remaining_accounts = vec![fee_payer_ai, config_ai, rent_sponsor_ai, system_ai, pda_ai]; + let params = one_pda_params(SkipVariant, 3); + let cpi_signer = CpiSigner { + program_id, + cpi_signer: [0u8; 32], + bump: 255, + }; + + let result = + build_decompress_pda_cpi_data(&remaining_accounts, ¶ms, cpi_signer, &program_id, 0); + + assert!(matches!( + result, + Err(LightSdkTypesError::ConstraintViolation) + )); +}