From 4158fb7b7c6b1c7725562c202f2b9a1ef5bb821f Mon Sep 17 00:00:00 2001 From: ananas Date: Mon, 5 Jan 2026 20:17:11 +0000 Subject: [PATCH 01/18] refactor: compressed token program folder structure into light_token, compressed_token --- .../mint_action/accounts.rs | 0 .../mint_action/actions/authority.rs | 0 .../actions/compress_and_close_cmint.rs | 2 +- .../mint_action/actions/create_mint.rs | 0 .../mint_action/actions/decompress_mint.rs | 4 +- .../mint_action/actions/mint_to.rs | 2 +- .../mint_action/actions/mint_to_ctoken.rs | 4 +- .../mint_action/actions/mod.rs | 0 .../mint_action/actions/process_actions.rs | 2 +- .../mint_action/actions/update_metadata.rs | 2 +- .../mint_action/mint_input.rs | 2 +- .../mint_action/mint_output.rs | 4 +- .../{ => compressed_token}/mint_action/mod.rs | 0 .../mint_action/processor.rs | 2 +- .../mint_action/queue_indices.rs | 0 .../mint_action/zero_copy_config.rs | 2 +- .../program/src/compressed_token/mod.rs | 2 + .../transfer2/accounts.rs | 2 +- .../transfer2/change_account.rs | 2 +- .../transfer2/check_extensions.rs | 0 .../compression/ctoken/compress_and_close.rs | 6 +-- .../ctoken/compress_or_decompress_ctokens.rs | 0 .../compression/ctoken/decompress.rs | 0 .../transfer2/compression/ctoken/inputs.rs | 0 .../transfer2/compression/ctoken/mod.rs | 0 .../transfer2/compression/mod.rs | 0 .../transfer2/compression/spl.rs | 0 .../transfer2/config.rs | 0 .../{ => compressed_token}/transfer2/cpi.rs | 0 .../{ => compressed_token}/transfer2/mod.rs | 0 .../transfer2/processor.rs | 4 +- .../transfer2/sum_check.rs | 0 .../transfer2/token_inputs.rs | 0 .../transfer2/token_outputs.rs | 0 .../program/src/{ => compressible}/claim.rs | 2 +- .../program/src/compressible/mod.rs | 5 +++ .../withdraw_funding_pool.rs | 2 +- programs/compressed-token/program/src/lib.rs | 41 +++++++------------ .../close_token_account/accounts.rs | 0 .../close_token_account/mod.rs | 0 .../close_token_account/processor.rs | 0 .../create_associated_token_account.rs | 2 +- .../{ => light_token}/create_token_account.rs | 0 .../ctoken_approve_revoke.rs | 2 +- .../src/{ => light_token}/ctoken_burn.rs | 0 .../{ => light_token}/ctoken_freeze_thaw.rs | 0 .../src/{ => light_token}/ctoken_mint_to.rs | 0 .../program/src/light_token/mod.rs | 21 ++++++++++ .../src/{ => light_token}/transfer/checked.rs | 0 .../src/{ => light_token}/transfer/default.rs | 2 +- .../src/{ => light_token}/transfer/mod.rs | 0 .../src/{ => light_token}/transfer/shared.rs | 0 .../program/src/shared/token_input.rs | 2 +- 53 files changed, 68 insertions(+), 53 deletions(-) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/accounts.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/actions/authority.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/actions/compress_and_close_cmint.rs (98%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/actions/create_mint.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/actions/decompress_mint.rs (98%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/actions/mint_to.rs (97%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/actions/mint_to_ctoken.rs (90%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/actions/mod.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/actions/process_actions.rs (99%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/actions/update_metadata.rs (98%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/mint_input.rs (95%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/mint_output.rs (99%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/mod.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/processor.rs (99%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/queue_indices.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/mint_action/zero_copy_config.rs (98%) create mode 100644 programs/compressed-token/program/src/compressed_token/mod.rs rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/accounts.rs (99%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/change_account.rs (98%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/check_extensions.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/compression/ctoken/compress_and_close.rs (98%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/compression/ctoken/decompress.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/compression/ctoken/inputs.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/compression/ctoken/mod.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/compression/mod.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/compression/spl.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/config.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/cpi.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/mod.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/processor.rs (99%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/sum_check.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/token_inputs.rs (100%) rename programs/compressed-token/program/src/{ => compressed_token}/transfer2/token_outputs.rs (100%) rename programs/compressed-token/program/src/{ => compressible}/claim.rs (98%) create mode 100644 programs/compressed-token/program/src/compressible/mod.rs rename programs/compressed-token/program/src/{ => compressible}/withdraw_funding_pool.rs (98%) rename programs/compressed-token/program/src/{ => light_token}/close_token_account/accounts.rs (100%) rename programs/compressed-token/program/src/{ => light_token}/close_token_account/mod.rs (100%) rename programs/compressed-token/program/src/{ => light_token}/close_token_account/processor.rs (100%) rename programs/compressed-token/program/src/{ => light_token}/create_associated_token_account.rs (99%) rename programs/compressed-token/program/src/{ => light_token}/create_token_account.rs (100%) rename programs/compressed-token/program/src/{ => light_token}/ctoken_approve_revoke.rs (99%) rename programs/compressed-token/program/src/{ => light_token}/ctoken_burn.rs (100%) rename programs/compressed-token/program/src/{ => light_token}/ctoken_freeze_thaw.rs (100%) rename programs/compressed-token/program/src/{ => light_token}/ctoken_mint_to.rs (100%) create mode 100644 programs/compressed-token/program/src/light_token/mod.rs rename programs/compressed-token/program/src/{ => light_token}/transfer/checked.rs (100%) rename programs/compressed-token/program/src/{ => light_token}/transfer/default.rs (97%) rename programs/compressed-token/program/src/{ => light_token}/transfer/mod.rs (100%) rename programs/compressed-token/program/src/{ => light_token}/transfer/shared.rs (100%) diff --git a/programs/compressed-token/program/src/mint_action/accounts.rs b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs similarity index 100% rename from programs/compressed-token/program/src/mint_action/accounts.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs diff --git a/programs/compressed-token/program/src/mint_action/actions/authority.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/authority.rs similarity index 100% rename from programs/compressed-token/program/src/mint_action/actions/authority.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/actions/authority.rs diff --git a/programs/compressed-token/program/src/mint_action/actions/compress_and_close_cmint.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs similarity index 98% rename from programs/compressed-token/program/src/mint_action/actions/compress_and_close_cmint.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs index 4434953a1c..9ea9f77194 100644 --- a/programs/compressed-token/program/src/mint_action/actions/compress_and_close_cmint.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs @@ -11,7 +11,7 @@ use pinocchio::{ use spl_pod::solana_msg::msg; use crate::{ - mint_action::accounts::MintActionAccounts, + compressed_token::mint_action::accounts::MintActionAccounts, shared::{convert_program_error, transfer_lamports::transfer_lamports}, }; diff --git a/programs/compressed-token/program/src/mint_action/actions/create_mint.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/create_mint.rs similarity index 100% rename from programs/compressed-token/program/src/mint_action/actions/create_mint.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/actions/create_mint.rs diff --git a/programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs similarity index 98% rename from programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs index b84bd1c955..c157558ceb 100644 --- a/programs/compressed-token/program/src/mint_action/actions/decompress_mint.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs @@ -14,8 +14,8 @@ use pinocchio_system::instructions::Transfer; use spl_pod::solana_msg::msg; use crate::{ - create_token_account::parse_config_account, - mint_action::accounts::MintActionAccounts, + compressed_token::mint_action::accounts::MintActionAccounts, + light_token::create_token_account::parse_config_account, shared::{ convert_program_error, create_pda_account::{create_pda_account, verify_pda}, diff --git a/programs/compressed-token/program/src/mint_action/actions/mint_to.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to.rs similarity index 97% rename from programs/compressed-token/program/src/mint_action/actions/mint_to.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to.rs index dc538f4f9b..9ec58c163c 100644 --- a/programs/compressed-token/program/src/mint_action/actions/mint_to.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to.rs @@ -9,7 +9,7 @@ use light_program_profiler::profile; use light_sdk_pinocchio::instruction::ZOutputCompressedAccountWithPackedContextMut; use crate::{ - mint_action::{accounts::MintActionAccounts, check_authority}, + compressed_token::mint_action::{accounts::MintActionAccounts, check_authority}, shared::token_output::set_output_compressed_account, }; diff --git a/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to_ctoken.rs similarity index 90% rename from programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to_ctoken.rs index 91001d5f4b..0945085351 100644 --- a/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to_ctoken.rs @@ -9,8 +9,8 @@ use light_program_profiler::profile; use pinocchio::account_info::AccountInfo; use crate::{ - mint_action::{accounts::MintActionAccounts, check_authority}, - transfer2::compression::{compress_or_decompress_ctokens, CTokenCompressionInputs}, + compressed_token::mint_action::{accounts::MintActionAccounts, check_authority}, + compressed_token::transfer2::compression::{compress_or_decompress_ctokens, CTokenCompressionInputs}, }; #[allow(clippy::too_many_arguments)] diff --git a/programs/compressed-token/program/src/mint_action/actions/mod.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/mod.rs similarity index 100% rename from programs/compressed-token/program/src/mint_action/actions/mod.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/actions/mod.rs diff --git a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs similarity index 99% rename from programs/compressed-token/program/src/mint_action/actions/process_actions.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs index 38a9bd0b72..a4c91f0285 100644 --- a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs @@ -14,7 +14,7 @@ use pinocchio::account_info::AccountInfo; use spl_pod::solana_msg::msg; use crate::{ - mint_action::{ + compressed_token::mint_action::{ accounts::MintActionAccounts, check_authority, compress_and_close_cmint::process_compress_and_close_cmint_action, diff --git a/programs/compressed-token/program/src/mint_action/actions/update_metadata.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/update_metadata.rs similarity index 98% rename from programs/compressed-token/program/src/mint_action/actions/update_metadata.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/actions/update_metadata.rs index 5048ee7bdc..d15bd9d840 100644 --- a/programs/compressed-token/program/src/mint_action/actions/update_metadata.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/update_metadata.rs @@ -10,7 +10,7 @@ use light_ctoken_interface::{ use light_program_profiler::profile; use spl_pod::solana_msg::msg; -use crate::mint_action::check_authority; +use crate::compressed_token::mint_action::check_authority; /// Get mutable reference to metadata extension at specified index #[profile] diff --git a/programs/compressed-token/program/src/mint_action/mint_input.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs similarity index 95% rename from programs/compressed-token/program/src/mint_action/mint_input.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs index ef4c8ff306..a9fd961722 100644 --- a/programs/compressed-token/program/src/mint_action/mint_input.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs @@ -8,7 +8,7 @@ use light_hasher::{sha256::Sha256BE, Hasher}; use light_program_profiler::profile; use light_sdk::instruction::PackedMerkleContext; -use crate::{constants::COMPRESSED_MINT_DISCRIMINATOR, mint_action::accounts::AccountsConfig}; +use crate::{compressed_token::mint_action::accounts::AccountsConfig, constants::COMPRESSED_MINT_DISCRIMINATOR}; /// Creates and validates an input compressed mint account. /// This function follows the same pattern as create_output_compressed_mint_account diff --git a/programs/compressed-token/program/src/mint_action/mint_output.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs similarity index 99% rename from programs/compressed-token/program/src/mint_action/mint_output.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs index d8cb7656d2..a1e227799b 100644 --- a/programs/compressed-token/program/src/mint_action/mint_output.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs @@ -13,12 +13,12 @@ use pinocchio::sysvars::{clock::Clock, rent::Rent, Sysvar}; use spl_pod::solana_msg::msg; use crate::{ - constants::COMPRESSED_MINT_DISCRIMINATOR, - mint_action::{ + compressed_token::mint_action::{ accounts::{AccountsConfig, MintActionAccounts}, actions::process_actions, queue_indices::QueueIndices, }, + constants::COMPRESSED_MINT_DISCRIMINATOR, shared::{convert_program_error, transfer_lamports::transfer_lamports}, }; diff --git a/programs/compressed-token/program/src/mint_action/mod.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mod.rs similarity index 100% rename from programs/compressed-token/program/src/mint_action/mod.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/mod.rs diff --git a/programs/compressed-token/program/src/mint_action/processor.rs b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs similarity index 99% rename from programs/compressed-token/program/src/mint_action/processor.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/processor.rs index 14ba3ec9dd..dfcbf64646 100644 --- a/programs/compressed-token/program/src/mint_action/processor.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs @@ -10,7 +10,7 @@ use light_zero_copy::{traits::ZeroCopyAt, ZeroCopyNew}; use pinocchio::account_info::AccountInfo; use crate::{ - mint_action::{ + compressed_token::mint_action::{ accounts::{AccountsConfig, MintActionAccounts}, create_mint::process_create_mint_action, mint_input::create_input_compressed_mint_account, diff --git a/programs/compressed-token/program/src/mint_action/queue_indices.rs b/programs/compressed-token/program/src/compressed_token/mint_action/queue_indices.rs similarity index 100% rename from programs/compressed-token/program/src/mint_action/queue_indices.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/queue_indices.rs diff --git a/programs/compressed-token/program/src/mint_action/zero_copy_config.rs b/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs similarity index 98% rename from programs/compressed-token/program/src/mint_action/zero_copy_config.rs rename to programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs index 6bd87282af..5beba029ac 100644 --- a/programs/compressed-token/program/src/mint_action/zero_copy_config.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs @@ -10,7 +10,7 @@ use spl_pod::solana_msg::msg; use tinyvec::ArrayVec; use crate::{ - mint_action::accounts::AccountsConfig, + compressed_token::mint_action::accounts::AccountsConfig, shared::{ convert_program_error, cpi_bytes_size::{ diff --git a/programs/compressed-token/program/src/compressed_token/mod.rs b/programs/compressed-token/program/src/compressed_token/mod.rs new file mode 100644 index 0000000000..ebc41da8c3 --- /dev/null +++ b/programs/compressed-token/program/src/compressed_token/mod.rs @@ -0,0 +1,2 @@ +pub mod mint_action; +pub mod transfer2; diff --git a/programs/compressed-token/program/src/transfer2/accounts.rs b/programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs similarity index 99% rename from programs/compressed-token/program/src/transfer2/accounts.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs index 5b47c229df..7a46e5bf5b 100644 --- a/programs/compressed-token/program/src/transfer2/accounts.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs @@ -6,11 +6,11 @@ use pinocchio::{account_info::AccountInfo, pubkey::Pubkey}; use spl_pod::solana_msg::msg; use crate::{ + compressed_token::transfer2::config::Transfer2Config, shared::{ accounts::{CpiContextLightSystemAccounts, LightSystemAccounts}, AccountIterator, }, - transfer2::config::Transfer2Config, }; /// 3 Scenarios: diff --git a/programs/compressed-token/program/src/transfer2/change_account.rs b/programs/compressed-token/program/src/compressed_token/transfer2/change_account.rs similarity index 98% rename from programs/compressed-token/program/src/transfer2/change_account.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/change_account.rs index 4c5bb6fdd4..3b35522c4d 100644 --- a/programs/compressed-token/program/src/transfer2/change_account.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/change_account.rs @@ -6,7 +6,7 @@ use light_compressed_account::instruction_data::with_readonly::ZInstructionDataI use light_ctoken_interface::instructions::transfer2::ZCompressedTokenInstructionDataTransfer2; use pinocchio::account_info::AccountInfo; -use crate::transfer2::config::Transfer2Config; +use crate::compressed_token::transfer2::config::Transfer2Config; /// Create a change account for excess lamports (following anchor program pattern) pub fn assign_change_account( diff --git a/programs/compressed-token/program/src/transfer2/check_extensions.rs b/programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/check_extensions.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs similarity index 98% rename from programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs index 62575eed0b..1c046dd6d3 100644 --- a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs @@ -19,10 +19,10 @@ use spl_pod::solana_msg::msg; use super::inputs::CompressAndCloseInputs; use crate::{ - close_token_account::{ + compressed_token::transfer2::accounts::Transfer2Accounts, + light_token::close_token_account::{ accounts::CloseTokenAccountAccounts, processor::validate_token_account_for_close_transfer2, }, - transfer2::accounts::Transfer2Accounts, }; /// Process compress and close operation for a ctoken account. @@ -335,7 +335,7 @@ pub fn close_for_compress_and_close( let authority = validated_accounts .packed_accounts .get_u8(compression.authority, "CompressAndClose: authority")?; - use crate::close_token_account::processor::close_token_account; + use crate::light_token::close_token_account::processor::close_token_account; close_token_account(&CloseTokenAccountAccounts { token_account: token_account_info, destination, diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/decompress.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/compression/ctoken/decompress.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/inputs.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/inputs.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/compression/ctoken/inputs.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/inputs.rs diff --git a/programs/compressed-token/program/src/transfer2/compression/ctoken/mod.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/mod.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/compression/ctoken/mod.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/mod.rs diff --git a/programs/compressed-token/program/src/transfer2/compression/mod.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/compression/mod.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs diff --git a/programs/compressed-token/program/src/transfer2/compression/spl.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/spl.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/compression/spl.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/compression/spl.rs diff --git a/programs/compressed-token/program/src/transfer2/config.rs b/programs/compressed-token/program/src/compressed_token/transfer2/config.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/config.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/config.rs diff --git a/programs/compressed-token/program/src/transfer2/cpi.rs b/programs/compressed-token/program/src/compressed_token/transfer2/cpi.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/cpi.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/cpi.rs diff --git a/programs/compressed-token/program/src/transfer2/mod.rs b/programs/compressed-token/program/src/compressed_token/transfer2/mod.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/mod.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/mod.rs diff --git a/programs/compressed-token/program/src/transfer2/processor.rs b/programs/compressed-token/program/src/compressed_token/transfer2/processor.rs similarity index 99% rename from programs/compressed-token/program/src/transfer2/processor.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/processor.rs index 18f49e9fe7..20fd0c0875 100644 --- a/programs/compressed-token/program/src/transfer2/processor.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/processor.rs @@ -20,8 +20,7 @@ use spl_pod::solana_msg::msg; use super::check_extensions::{build_mint_extension_cache, MintExtensionCache}; use crate::{ - shared::{convert_program_error, cpi::execute_cpi_invoke}, - transfer2::{ + compressed_token::transfer2::{ accounts::Transfer2Accounts, compression::{close_for_compress_and_close, process_token_compression}, config::Transfer2Config, @@ -30,6 +29,7 @@ use crate::{ token_inputs::set_input_compressed_accounts, token_outputs::set_output_compressed_accounts, }, + shared::{convert_program_error, cpi::execute_cpi_invoke}, }; /// Process a token transfer instruction diff --git a/programs/compressed-token/program/src/transfer2/sum_check.rs b/programs/compressed-token/program/src/compressed_token/transfer2/sum_check.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/sum_check.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/sum_check.rs diff --git a/programs/compressed-token/program/src/transfer2/token_inputs.rs b/programs/compressed-token/program/src/compressed_token/transfer2/token_inputs.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/token_inputs.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/token_inputs.rs diff --git a/programs/compressed-token/program/src/transfer2/token_outputs.rs b/programs/compressed-token/program/src/compressed_token/transfer2/token_outputs.rs similarity index 100% rename from programs/compressed-token/program/src/transfer2/token_outputs.rs rename to programs/compressed-token/program/src/compressed_token/transfer2/token_outputs.rs diff --git a/programs/compressed-token/program/src/claim.rs b/programs/compressed-token/program/src/compressible/claim.rs similarity index 98% rename from programs/compressed-token/program/src/claim.rs rename to programs/compressed-token/program/src/compressible/claim.rs index 8b5eef1203..d4330d5a8a 100644 --- a/programs/compressed-token/program/src/claim.rs +++ b/programs/compressed-token/program/src/compressible/claim.rs @@ -11,7 +11,7 @@ use pinocchio::{account_info::AccountInfo, sysvars::Sysvar}; use spl_pod::solana_msg::msg; use crate::{ - create_token_account::parse_config_account, + light_token::create_token_account::parse_config_account, shared::{convert_program_error, transfer_lamports}, }; diff --git a/programs/compressed-token/program/src/compressible/mod.rs b/programs/compressed-token/program/src/compressible/mod.rs new file mode 100644 index 0000000000..d1aeb32ff6 --- /dev/null +++ b/programs/compressed-token/program/src/compressible/mod.rs @@ -0,0 +1,5 @@ +pub mod claim; +pub mod withdraw_funding_pool; + +pub use claim::process_claim; +pub use withdraw_funding_pool::process_withdraw_funding_pool; diff --git a/programs/compressed-token/program/src/withdraw_funding_pool.rs b/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs similarity index 98% rename from programs/compressed-token/program/src/withdraw_funding_pool.rs rename to programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs index 7d1bd1ddd7..1a53277e47 100644 --- a/programs/compressed-token/program/src/withdraw_funding_pool.rs +++ b/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs @@ -8,7 +8,7 @@ use pinocchio::{ use pinocchio_system::instructions::Transfer; use spl_pod::solana_msg::msg; -use crate::create_token_account::parse_config_account; +use crate::light_token::create_token_account::parse_config_account; /// Accounts required for the withdraw funding pool instruction pub struct WithdrawFundingPoolAccounts<'a> { diff --git a/programs/compressed-token/program/src/lib.rs b/programs/compressed-token/program/src/lib.rs index b78e633f85..94ba35e74d 100644 --- a/programs/compressed-token/program/src/lib.rs +++ b/programs/compressed-token/program/src/lib.rs @@ -5,41 +5,28 @@ use light_ctoken_interface::CTOKEN_PROGRAM_ID; use light_sdk::{cpi::CpiSigner, derive_light_cpi_signer}; use pinocchio::{account_info::AccountInfo, msg}; -pub mod claim; -pub mod close_token_account; +pub mod compressed_token; +pub mod compressible; pub mod convert_account_infos; -pub mod create_associated_token_account; -pub mod create_token_account; -pub mod ctoken_approve_revoke; -pub mod ctoken_burn; -pub mod ctoken_freeze_thaw; -pub mod ctoken_mint_to; pub mod extensions; -pub mod mint_action; +pub mod light_token; pub mod shared; -pub mod transfer; -pub mod transfer2; -pub mod withdraw_funding_pool; // Reexport the wrapped anchor program. pub use ::anchor_compressed_token::*; -use claim::process_claim; -use close_token_account::processor::process_close_token_account; -use create_associated_token_account::{ - process_create_associated_token_account, process_create_associated_token_account_idempotent, +use compressible::{process_claim, process_withdraw_funding_pool}; +use light_token::{ + process_close_token_account, process_create_associated_token_account, + process_create_associated_token_account_idempotent, process_create_token_account, + process_ctoken_approve, process_ctoken_approve_checked, process_ctoken_burn, + process_ctoken_burn_checked, process_ctoken_freeze_account, process_ctoken_mint_to, + process_ctoken_mint_to_checked, process_ctoken_revoke, process_ctoken_thaw_account, + process_ctoken_transfer, process_ctoken_transfer_checked, }; -use create_token_account::process_create_token_account; -use ctoken_approve_revoke::{ - process_ctoken_approve, process_ctoken_approve_checked, process_ctoken_revoke, -}; -use ctoken_burn::{process_ctoken_burn, process_ctoken_burn_checked}; -use ctoken_freeze_thaw::{process_ctoken_freeze_account, process_ctoken_thaw_account}; -use ctoken_mint_to::{process_ctoken_mint_to, process_ctoken_mint_to_checked}; -use transfer::{process_ctoken_transfer, process_ctoken_transfer_checked}; -use withdraw_funding_pool::process_withdraw_funding_pool; use crate::{ - convert_account_infos::convert_account_infos, mint_action::processor::process_mint_action, + compressed_token::mint_action::processor::process_mint_action, + convert_account_infos::convert_account_infos, }; pub const LIGHT_CPI_SIGNER: CpiSigner = @@ -135,7 +122,7 @@ impl From for InstructionType { #[cfg(not(feature = "cpi"))] use pinocchio::program_entrypoint; -use crate::transfer2::processor::process_transfer2; +use crate::compressed_token::transfer2::processor::process_transfer2; #[cfg(not(feature = "cpi"))] program_entrypoint!(process_instruction); diff --git a/programs/compressed-token/program/src/close_token_account/accounts.rs b/programs/compressed-token/program/src/light_token/close_token_account/accounts.rs similarity index 100% rename from programs/compressed-token/program/src/close_token_account/accounts.rs rename to programs/compressed-token/program/src/light_token/close_token_account/accounts.rs diff --git a/programs/compressed-token/program/src/close_token_account/mod.rs b/programs/compressed-token/program/src/light_token/close_token_account/mod.rs similarity index 100% rename from programs/compressed-token/program/src/close_token_account/mod.rs rename to programs/compressed-token/program/src/light_token/close_token_account/mod.rs diff --git a/programs/compressed-token/program/src/close_token_account/processor.rs b/programs/compressed-token/program/src/light_token/close_token_account/processor.rs similarity index 100% rename from programs/compressed-token/program/src/close_token_account/processor.rs rename to programs/compressed-token/program/src/light_token/close_token_account/processor.rs diff --git a/programs/compressed-token/program/src/create_associated_token_account.rs b/programs/compressed-token/program/src/light_token/create_associated_token_account.rs similarity index 99% rename from programs/compressed-token/program/src/create_associated_token_account.rs rename to programs/compressed-token/program/src/light_token/create_associated_token_account.rs index 07eb70e46e..002508d36b 100644 --- a/programs/compressed-token/program/src/create_associated_token_account.rs +++ b/programs/compressed-token/program/src/light_token/create_associated_token_account.rs @@ -7,8 +7,8 @@ use pinocchio::{account_info::AccountInfo, instruction::Seed}; use spl_pod::solana_msg::msg; use crate::{ - create_token_account::next_config_account, extensions::has_mint_extensions, + light_token::create_token_account::next_config_account, shared::{ convert_program_error, create_pda_account, initialize_ctoken_account::{ diff --git a/programs/compressed-token/program/src/create_token_account.rs b/programs/compressed-token/program/src/light_token/create_token_account.rs similarity index 100% rename from programs/compressed-token/program/src/create_token_account.rs rename to programs/compressed-token/program/src/light_token/create_token_account.rs diff --git a/programs/compressed-token/program/src/ctoken_approve_revoke.rs b/programs/compressed-token/program/src/light_token/ctoken_approve_revoke.rs similarity index 99% rename from programs/compressed-token/program/src/ctoken_approve_revoke.rs rename to programs/compressed-token/program/src/light_token/ctoken_approve_revoke.rs index f532cc684f..cb54ec26b1 100644 --- a/programs/compressed-token/program/src/ctoken_approve_revoke.rs +++ b/programs/compressed-token/program/src/light_token/ctoken_approve_revoke.rs @@ -7,11 +7,11 @@ use pinocchio_token_program::processor::{ }; use crate::{ + compressed_token::transfer2::compression::ctoken::process_compression_top_up, shared::{ convert_program_error, owner_validation::check_token_program_owner, transfer_lamports_via_cpi, }, - transfer2::compression::ctoken::process_compression_top_up, }; /// Account indices for approve instruction diff --git a/programs/compressed-token/program/src/ctoken_burn.rs b/programs/compressed-token/program/src/light_token/ctoken_burn.rs similarity index 100% rename from programs/compressed-token/program/src/ctoken_burn.rs rename to programs/compressed-token/program/src/light_token/ctoken_burn.rs diff --git a/programs/compressed-token/program/src/ctoken_freeze_thaw.rs b/programs/compressed-token/program/src/light_token/ctoken_freeze_thaw.rs similarity index 100% rename from programs/compressed-token/program/src/ctoken_freeze_thaw.rs rename to programs/compressed-token/program/src/light_token/ctoken_freeze_thaw.rs diff --git a/programs/compressed-token/program/src/ctoken_mint_to.rs b/programs/compressed-token/program/src/light_token/ctoken_mint_to.rs similarity index 100% rename from programs/compressed-token/program/src/ctoken_mint_to.rs rename to programs/compressed-token/program/src/light_token/ctoken_mint_to.rs diff --git a/programs/compressed-token/program/src/light_token/mod.rs b/programs/compressed-token/program/src/light_token/mod.rs new file mode 100644 index 0000000000..9d900191ea --- /dev/null +++ b/programs/compressed-token/program/src/light_token/mod.rs @@ -0,0 +1,21 @@ +pub mod close_token_account; +pub mod create_associated_token_account; +pub mod create_token_account; +pub mod ctoken_approve_revoke; +pub mod ctoken_burn; +pub mod ctoken_freeze_thaw; +pub mod ctoken_mint_to; +pub mod transfer; + +pub use close_token_account::processor::process_close_token_account; +pub use create_associated_token_account::{ + process_create_associated_token_account, process_create_associated_token_account_idempotent, +}; +pub use create_token_account::process_create_token_account; +pub use ctoken_approve_revoke::{ + process_ctoken_approve, process_ctoken_approve_checked, process_ctoken_revoke, +}; +pub use ctoken_burn::{process_ctoken_burn, process_ctoken_burn_checked}; +pub use ctoken_freeze_thaw::{process_ctoken_freeze_account, process_ctoken_thaw_account}; +pub use ctoken_mint_to::{process_ctoken_mint_to, process_ctoken_mint_to_checked}; +pub use transfer::{process_ctoken_transfer, process_ctoken_transfer_checked}; diff --git a/programs/compressed-token/program/src/transfer/checked.rs b/programs/compressed-token/program/src/light_token/transfer/checked.rs similarity index 100% rename from programs/compressed-token/program/src/transfer/checked.rs rename to programs/compressed-token/program/src/light_token/transfer/checked.rs diff --git a/programs/compressed-token/program/src/transfer/default.rs b/programs/compressed-token/program/src/light_token/transfer/default.rs similarity index 97% rename from programs/compressed-token/program/src/transfer/default.rs rename to programs/compressed-token/program/src/light_token/transfer/default.rs index 37af0ce4eb..02cb3bb94e 100644 --- a/programs/compressed-token/program/src/transfer/default.rs +++ b/programs/compressed-token/program/src/light_token/transfer/default.rs @@ -3,7 +3,7 @@ use light_program_profiler::profile; use pinocchio::account_info::AccountInfo; use pinocchio_token_program::processor::transfer::process_transfer; -use crate::transfer::shared::{process_transfer_extensions_transfer, TransferAccounts}; +use super::shared::{process_transfer_extensions_transfer, TransferAccounts}; /// Account indices for CToken transfer instruction const ACCOUNT_SOURCE: usize = 0; diff --git a/programs/compressed-token/program/src/transfer/mod.rs b/programs/compressed-token/program/src/light_token/transfer/mod.rs similarity index 100% rename from programs/compressed-token/program/src/transfer/mod.rs rename to programs/compressed-token/program/src/light_token/transfer/mod.rs diff --git a/programs/compressed-token/program/src/transfer/shared.rs b/programs/compressed-token/program/src/light_token/transfer/shared.rs similarity index 100% rename from programs/compressed-token/program/src/transfer/shared.rs rename to programs/compressed-token/program/src/light_token/transfer/shared.rs diff --git a/programs/compressed-token/program/src/shared/token_input.rs b/programs/compressed-token/program/src/shared/token_input.rs index 4cba7ec723..fb704cda37 100644 --- a/programs/compressed-token/program/src/shared/token_input.rs +++ b/programs/compressed-token/program/src/shared/token_input.rs @@ -16,8 +16,8 @@ use light_ctoken_interface::{ use pinocchio::account_info::AccountInfo; use crate::{ + compressed_token::transfer2::check_extensions::MintExtensionCache, shared::owner_validation::verify_owner_or_delegate_signer, - transfer2::check_extensions::MintExtensionCache, }; /// Creates an input compressed account using zero-copy patterns and index-based account lookup. From c17726b4765c54449384a6f77b73ab26cce4d7ba Mon Sep 17 00:00:00 2001 From: ananas Date: Mon, 5 Jan 2026 20:26:11 +0000 Subject: [PATCH 02/18] refactor: remove ctoken prefix from file names --- .../ctoken-interface/src/state/ctoken/size.rs | 4 ++-- .../mint_action/actions/decompress_mint.rs | 2 +- .../mint_action/actions/mint_to_ctoken.rs | 6 +++--- .../mint_action/mint_input.rs | 5 ++++- .../compression/ctoken/compress_and_close.rs | 4 ++-- .../program/src/compressible/claim.rs | 2 +- .../src/compressible/withdraw_funding_pool.rs | 2 +- .../approve_revoke.rs} | 0 .../ctoken_burn.rs => ctoken/burn.rs} | 0 .../close}/accounts.rs | 0 .../close}/mod.rs | 0 .../close}/processor.rs | 0 .../create.rs} | 0 .../create_ata.rs} | 2 +- .../freeze_thaw.rs} | 0 .../ctoken_mint_to.rs => ctoken/mint_to.rs} | 0 .../program/src/ctoken/mod.rs | 21 +++++++++++++++++++ .../transfer/checked.rs | 0 .../transfer/default.rs | 0 .../{light_token => ctoken}/transfer/mod.rs | 0 .../transfer/shared.rs | 0 programs/compressed-token/program/src/lib.rs | 4 ++-- .../program/src/light_token/mod.rs | 21 ------------------- 23 files changed, 38 insertions(+), 35 deletions(-) rename programs/compressed-token/program/src/{light_token/ctoken_approve_revoke.rs => ctoken/approve_revoke.rs} (100%) rename programs/compressed-token/program/src/{light_token/ctoken_burn.rs => ctoken/burn.rs} (100%) rename programs/compressed-token/program/src/{light_token/close_token_account => ctoken/close}/accounts.rs (100%) rename programs/compressed-token/program/src/{light_token/close_token_account => ctoken/close}/mod.rs (100%) rename programs/compressed-token/program/src/{light_token/close_token_account => ctoken/close}/processor.rs (100%) rename programs/compressed-token/program/src/{light_token/create_token_account.rs => ctoken/create.rs} (100%) rename programs/compressed-token/program/src/{light_token/create_associated_token_account.rs => ctoken/create_ata.rs} (99%) rename programs/compressed-token/program/src/{light_token/ctoken_freeze_thaw.rs => ctoken/freeze_thaw.rs} (100%) rename programs/compressed-token/program/src/{light_token/ctoken_mint_to.rs => ctoken/mint_to.rs} (100%) create mode 100644 programs/compressed-token/program/src/ctoken/mod.rs rename programs/compressed-token/program/src/{light_token => ctoken}/transfer/checked.rs (100%) rename programs/compressed-token/program/src/{light_token => ctoken}/transfer/default.rs (100%) rename programs/compressed-token/program/src/{light_token => ctoken}/transfer/mod.rs (100%) rename programs/compressed-token/program/src/{light_token => ctoken}/transfer/shared.rs (100%) delete mode 100644 programs/compressed-token/program/src/light_token/mod.rs diff --git a/program-libs/ctoken-interface/src/state/ctoken/size.rs b/program-libs/ctoken-interface/src/state/ctoken/size.rs index e53a6da551..e4c1dcba99 100644 --- a/program-libs/ctoken-interface/src/state/ctoken/size.rs +++ b/program-libs/ctoken-interface/src/state/ctoken/size.rs @@ -7,8 +7,8 @@ use crate::{ /// Calculates the size of a ctoken account based on which extensions are present. /// -/// Note: Compression info is now embedded in the base struct (CTokenZeroCopyMeta), -/// so there's no separate compressible extension parameter. +/// Note: Compressible extension is required if the T22 mint has restricted extensions +/// (Pausable, PermanentDelegate, TransferFee, TransferHook). /// /// # Arguments /// * `extensions` - Optional slice of extension configs diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs index c157558ceb..db3261f79a 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs @@ -15,7 +15,7 @@ use spl_pod::solana_msg::msg; use crate::{ compressed_token::mint_action::accounts::MintActionAccounts, - light_token::create_token_account::parse_config_account, + ctoken::create::parse_config_account, shared::{ convert_program_error, create_pda_account::{create_pda_account, verify_pda}, diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to_ctoken.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to_ctoken.rs index 0945085351..03ce5c21eb 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to_ctoken.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to_ctoken.rs @@ -8,9 +8,9 @@ use light_ctoken_interface::{ use light_program_profiler::profile; use pinocchio::account_info::AccountInfo; -use crate::{ - compressed_token::mint_action::{accounts::MintActionAccounts, check_authority}, - compressed_token::transfer2::compression::{compress_or_decompress_ctokens, CTokenCompressionInputs}, +use crate::compressed_token::{ + mint_action::{accounts::MintActionAccounts, check_authority}, + transfer2::compression::{compress_or_decompress_ctokens, CTokenCompressionInputs}, }; #[allow(clippy::too_many_arguments)] diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs index a9fd961722..a48de9e63b 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs @@ -8,7 +8,10 @@ use light_hasher::{sha256::Sha256BE, Hasher}; use light_program_profiler::profile; use light_sdk::instruction::PackedMerkleContext; -use crate::{compressed_token::mint_action::accounts::AccountsConfig, constants::COMPRESSED_MINT_DISCRIMINATOR}; +use crate::{ + compressed_token::mint_action::accounts::AccountsConfig, + constants::COMPRESSED_MINT_DISCRIMINATOR, +}; /// Creates and validates an input compressed mint account. /// This function follows the same pattern as create_output_compressed_mint_account diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs index 1c046dd6d3..29d8816861 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs @@ -20,7 +20,7 @@ use spl_pod::solana_msg::msg; use super::inputs::CompressAndCloseInputs; use crate::{ compressed_token::transfer2::accounts::Transfer2Accounts, - light_token::close_token_account::{ + ctoken::close::{ accounts::CloseTokenAccountAccounts, processor::validate_token_account_for_close_transfer2, }, }; @@ -335,7 +335,7 @@ pub fn close_for_compress_and_close( let authority = validated_accounts .packed_accounts .get_u8(compression.authority, "CompressAndClose: authority")?; - use crate::light_token::close_token_account::processor::close_token_account; + use crate::ctoken::close::processor::close_token_account; close_token_account(&CloseTokenAccountAccounts { token_account: token_account_info, destination, diff --git a/programs/compressed-token/program/src/compressible/claim.rs b/programs/compressed-token/program/src/compressible/claim.rs index d4330d5a8a..b5129926d9 100644 --- a/programs/compressed-token/program/src/compressible/claim.rs +++ b/programs/compressed-token/program/src/compressible/claim.rs @@ -11,7 +11,7 @@ use pinocchio::{account_info::AccountInfo, sysvars::Sysvar}; use spl_pod::solana_msg::msg; use crate::{ - light_token::create_token_account::parse_config_account, + ctoken::create::parse_config_account, shared::{convert_program_error, transfer_lamports}, }; diff --git a/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs b/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs index 1a53277e47..8cefc06d08 100644 --- a/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs +++ b/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs @@ -8,7 +8,7 @@ use pinocchio::{ use pinocchio_system::instructions::Transfer; use spl_pod::solana_msg::msg; -use crate::light_token::create_token_account::parse_config_account; +use crate::ctoken::create::parse_config_account; /// Accounts required for the withdraw funding pool instruction pub struct WithdrawFundingPoolAccounts<'a> { diff --git a/programs/compressed-token/program/src/light_token/ctoken_approve_revoke.rs b/programs/compressed-token/program/src/ctoken/approve_revoke.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/ctoken_approve_revoke.rs rename to programs/compressed-token/program/src/ctoken/approve_revoke.rs diff --git a/programs/compressed-token/program/src/light_token/ctoken_burn.rs b/programs/compressed-token/program/src/ctoken/burn.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/ctoken_burn.rs rename to programs/compressed-token/program/src/ctoken/burn.rs diff --git a/programs/compressed-token/program/src/light_token/close_token_account/accounts.rs b/programs/compressed-token/program/src/ctoken/close/accounts.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/close_token_account/accounts.rs rename to programs/compressed-token/program/src/ctoken/close/accounts.rs diff --git a/programs/compressed-token/program/src/light_token/close_token_account/mod.rs b/programs/compressed-token/program/src/ctoken/close/mod.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/close_token_account/mod.rs rename to programs/compressed-token/program/src/ctoken/close/mod.rs diff --git a/programs/compressed-token/program/src/light_token/close_token_account/processor.rs b/programs/compressed-token/program/src/ctoken/close/processor.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/close_token_account/processor.rs rename to programs/compressed-token/program/src/ctoken/close/processor.rs diff --git a/programs/compressed-token/program/src/light_token/create_token_account.rs b/programs/compressed-token/program/src/ctoken/create.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/create_token_account.rs rename to programs/compressed-token/program/src/ctoken/create.rs diff --git a/programs/compressed-token/program/src/light_token/create_associated_token_account.rs b/programs/compressed-token/program/src/ctoken/create_ata.rs similarity index 99% rename from programs/compressed-token/program/src/light_token/create_associated_token_account.rs rename to programs/compressed-token/program/src/ctoken/create_ata.rs index 002508d36b..4811d6eb4e 100644 --- a/programs/compressed-token/program/src/light_token/create_associated_token_account.rs +++ b/programs/compressed-token/program/src/ctoken/create_ata.rs @@ -6,9 +6,9 @@ use light_program_profiler::profile; use pinocchio::{account_info::AccountInfo, instruction::Seed}; use spl_pod::solana_msg::msg; +use super::create::next_config_account; use crate::{ extensions::has_mint_extensions, - light_token::create_token_account::next_config_account, shared::{ convert_program_error, create_pda_account, initialize_ctoken_account::{ diff --git a/programs/compressed-token/program/src/light_token/ctoken_freeze_thaw.rs b/programs/compressed-token/program/src/ctoken/freeze_thaw.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/ctoken_freeze_thaw.rs rename to programs/compressed-token/program/src/ctoken/freeze_thaw.rs diff --git a/programs/compressed-token/program/src/light_token/ctoken_mint_to.rs b/programs/compressed-token/program/src/ctoken/mint_to.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/ctoken_mint_to.rs rename to programs/compressed-token/program/src/ctoken/mint_to.rs diff --git a/programs/compressed-token/program/src/ctoken/mod.rs b/programs/compressed-token/program/src/ctoken/mod.rs new file mode 100644 index 0000000000..441bda4979 --- /dev/null +++ b/programs/compressed-token/program/src/ctoken/mod.rs @@ -0,0 +1,21 @@ +pub mod approve_revoke; +pub mod burn; +pub mod close; +pub mod create; +pub mod create_ata; +pub mod freeze_thaw; +pub mod mint_to; +pub mod transfer; + +pub use approve_revoke::{ + process_ctoken_approve, process_ctoken_approve_checked, process_ctoken_revoke, +}; +pub use burn::{process_ctoken_burn, process_ctoken_burn_checked}; +pub use close::processor::process_close_token_account; +pub use create::process_create_token_account; +pub use create_ata::{ + process_create_associated_token_account, process_create_associated_token_account_idempotent, +}; +pub use freeze_thaw::{process_ctoken_freeze_account, process_ctoken_thaw_account}; +pub use mint_to::{process_ctoken_mint_to, process_ctoken_mint_to_checked}; +pub use transfer::{process_ctoken_transfer, process_ctoken_transfer_checked}; diff --git a/programs/compressed-token/program/src/light_token/transfer/checked.rs b/programs/compressed-token/program/src/ctoken/transfer/checked.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/transfer/checked.rs rename to programs/compressed-token/program/src/ctoken/transfer/checked.rs diff --git a/programs/compressed-token/program/src/light_token/transfer/default.rs b/programs/compressed-token/program/src/ctoken/transfer/default.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/transfer/default.rs rename to programs/compressed-token/program/src/ctoken/transfer/default.rs diff --git a/programs/compressed-token/program/src/light_token/transfer/mod.rs b/programs/compressed-token/program/src/ctoken/transfer/mod.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/transfer/mod.rs rename to programs/compressed-token/program/src/ctoken/transfer/mod.rs diff --git a/programs/compressed-token/program/src/light_token/transfer/shared.rs b/programs/compressed-token/program/src/ctoken/transfer/shared.rs similarity index 100% rename from programs/compressed-token/program/src/light_token/transfer/shared.rs rename to programs/compressed-token/program/src/ctoken/transfer/shared.rs diff --git a/programs/compressed-token/program/src/lib.rs b/programs/compressed-token/program/src/lib.rs index 94ba35e74d..16fda9cb7e 100644 --- a/programs/compressed-token/program/src/lib.rs +++ b/programs/compressed-token/program/src/lib.rs @@ -8,14 +8,14 @@ use pinocchio::{account_info::AccountInfo, msg}; pub mod compressed_token; pub mod compressible; pub mod convert_account_infos; +pub mod ctoken; pub mod extensions; -pub mod light_token; pub mod shared; // Reexport the wrapped anchor program. pub use ::anchor_compressed_token::*; use compressible::{process_claim, process_withdraw_funding_pool}; -use light_token::{ +use ctoken::{ process_close_token_account, process_create_associated_token_account, process_create_associated_token_account_idempotent, process_create_token_account, process_ctoken_approve, process_ctoken_approve_checked, process_ctoken_burn, diff --git a/programs/compressed-token/program/src/light_token/mod.rs b/programs/compressed-token/program/src/light_token/mod.rs deleted file mode 100644 index 9d900191ea..0000000000 --- a/programs/compressed-token/program/src/light_token/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub mod close_token_account; -pub mod create_associated_token_account; -pub mod create_token_account; -pub mod ctoken_approve_revoke; -pub mod ctoken_burn; -pub mod ctoken_freeze_thaw; -pub mod ctoken_mint_to; -pub mod transfer; - -pub use close_token_account::processor::process_close_token_account; -pub use create_associated_token_account::{ - process_create_associated_token_account, process_create_associated_token_account_idempotent, -}; -pub use create_token_account::process_create_token_account; -pub use ctoken_approve_revoke::{ - process_ctoken_approve, process_ctoken_approve_checked, process_ctoken_revoke, -}; -pub use ctoken_burn::{process_ctoken_burn, process_ctoken_burn_checked}; -pub use ctoken_freeze_thaw::{process_ctoken_freeze_account, process_ctoken_thaw_account}; -pub use ctoken_mint_to::{process_ctoken_mint_to, process_ctoken_mint_to_checked}; -pub use transfer::{process_ctoken_transfer, process_ctoken_transfer_checked}; From d567e6e4ee712fedc06a68a1874b3978eac36b99 Mon Sep 17 00:00:00 2001 From: ananas Date: Mon, 5 Jan 2026 20:35:49 +0000 Subject: [PATCH 03/18] refactor: compressed token program docs structure --- programs/compressed-token/program/CLAUDE.md | 62 ++++++++++---- .../compressed-token/program/docs/CLAUDE.md | 40 ++++++---- .../program/docs/EXTENSIONS.md | 10 +-- .../CLAUDE.md => INSTRUCTIONS.md} | 80 ++++++++++--------- .../ADD_TOKEN_POOL.md | 0 .../CREATE_TOKEN_POOL.md | 0 .../compressed_token/FREEZE.md | 0 .../MINT_ACTION.md | 2 +- .../compressed_token/THAW.md | 0 .../TRANSFER2.md | 2 +- .../{instructions => compressible}/CLAIM.md | 2 +- .../WITHDRAW_FUNDING_POOL.md | 2 +- .../CTOKEN_APPROVE.md => ctoken/APPROVE.md} | 4 +- .../APPROVE_CHECKED.md} | 4 +- .../CTOKEN_BURN.md => ctoken/BURN.md} | 2 +- .../BURN_CHECKED.md} | 2 +- .../CLOSE.md} | 2 +- .../CREATE.md} | 4 +- .../FREEZE_ACCOUNT.md} | 2 +- .../CTOKEN_MINT_TO.md => ctoken/MINT_TO.md} | 4 +- .../MINT_TO_CHECKED.md} | 4 +- .../CTOKEN_REVOKE.md => ctoken/REVOKE.md} | 4 +- .../THAW_ACCOUNT.md} | 2 +- .../CTOKEN_TRANSFER.md => ctoken/TRANSFER.md} | 2 +- .../TRANSFER_CHECKED.md} | 2 +- 25 files changed, 139 insertions(+), 99 deletions(-) rename programs/compressed-token/program/docs/{instructions/CLAUDE.md => INSTRUCTIONS.md} (52%) rename programs/compressed-token/program/docs/{instructions => compressed_token}/ADD_TOKEN_POOL.md (100%) rename programs/compressed-token/program/docs/{instructions => compressed_token}/CREATE_TOKEN_POOL.md (100%) rename programs/compressed-token/program/docs/{instructions => }/compressed_token/FREEZE.md (100%) rename programs/compressed-token/program/docs/{instructions => compressed_token}/MINT_ACTION.md (99%) rename programs/compressed-token/program/docs/{instructions => }/compressed_token/THAW.md (100%) rename programs/compressed-token/program/docs/{instructions => compressed_token}/TRANSFER2.md (99%) rename programs/compressed-token/program/docs/{instructions => compressible}/CLAIM.md (98%) rename programs/compressed-token/program/docs/{instructions => compressible}/WITHDRAW_FUNDING_POOL.md (97%) rename programs/compressed-token/program/docs/{instructions/CTOKEN_APPROVE.md => ctoken/APPROVE.md} (98%) rename programs/compressed-token/program/docs/{instructions/CTOKEN_APPROVE_CHECKED.md => ctoken/APPROVE_CHECKED.md} (98%) rename programs/compressed-token/program/docs/{instructions/CTOKEN_BURN.md => ctoken/BURN.md} (99%) rename programs/compressed-token/program/docs/{instructions/CTOKEN_BURN_CHECKED.md => ctoken/BURN_CHECKED.md} (99%) rename programs/compressed-token/program/docs/{instructions/CLOSE_TOKEN_ACCOUNT.md => ctoken/CLOSE.md} (99%) rename programs/compressed-token/program/docs/{instructions/CREATE_TOKEN_ACCOUNT.md => ctoken/CREATE.md} (98%) rename programs/compressed-token/program/docs/{instructions/CTOKEN_FREEZE_ACCOUNT.md => ctoken/FREEZE_ACCOUNT.md} (98%) rename programs/compressed-token/program/docs/{instructions/CTOKEN_MINT_TO.md => ctoken/MINT_TO.md} (98%) rename programs/compressed-token/program/docs/{instructions/CTOKEN_MINT_TO_CHECKED.md => ctoken/MINT_TO_CHECKED.md} (97%) rename programs/compressed-token/program/docs/{instructions/CTOKEN_REVOKE.md => ctoken/REVOKE.md} (97%) rename programs/compressed-token/program/docs/{instructions/CTOKEN_THAW_ACCOUNT.md => ctoken/THAW_ACCOUNT.md} (98%) rename programs/compressed-token/program/docs/{instructions/CTOKEN_TRANSFER.md => ctoken/TRANSFER.md} (98%) rename programs/compressed-token/program/docs/{instructions/CTOKEN_TRANSFER_CHECKED.md => ctoken/TRANSFER_CHECKED.md} (99%) diff --git a/programs/compressed-token/program/CLAUDE.md b/programs/compressed-token/program/CLAUDE.md index 2c40ce208a..34b21a916c 100644 --- a/programs/compressed-token/program/CLAUDE.md +++ b/programs/compressed-token/program/CLAUDE.md @@ -115,29 +115,59 @@ Every instruction description must include the sections: # Source Code Structure (`src/`) -## Core Instructions -- **`create_token_account.rs`** - Create regular ctoken accounts with optional compressible extension -- **`create_associated_token_account.rs`** - Create deterministic ATA accounts -- **`close_token_account/`** - Close ctoken accounts, handle rent distribution -- **`transfer/`** - SPL-compatible transfers between decompressed accounts - - `default.rs` - CTokenTransfer (discriminator: 3) - - `checked.rs` - CTokenTransferChecked (discriminator: 12) - - `shared.rs` - Common transfer utilities - -## Token Operations +``` +src/ +├── compressed_token/ # Operations on compressed accounts (in Merkle trees) +│ ├── mint_action/ # MintAction instruction (103) +│ └── transfer2/ # Transfer2 instruction (101) +├── compressible/ # Rent management +│ ├── claim.rs # Claim instruction (104) +│ └── withdraw_funding_pool.rs # WithdrawFundingPool instruction (105) +├── ctoken/ # Operations on CToken Solana accounts (decompressed) +│ ├── approve_revoke.rs # CTokenApprove (4), CTokenRevoke (5), CTokenApproveChecked (13) +│ ├── burn.rs # CTokenBurn (8), CTokenBurnChecked (15) +│ ├── close/ # CloseTokenAccount instruction (9) +│ ├── create.rs # CreateTokenAccount instruction (18) +│ ├── create_ata.rs # CreateAssociatedCTokenAccount (100, 102) +│ ├── freeze_thaw.rs # CTokenFreezeAccount (10), CTokenThawAccount (11) +│ ├── mint_to.rs # CTokenMintTo (7), CTokenMintToChecked (14) +│ └── transfer/ # CTokenTransfer (3), CTokenTransferChecked (12) +├── extensions/ # Extension handling +├── shared/ # Common utilities +├── convert_account_infos.rs +└── lib.rs # Entry point and instruction dispatch +``` + +## Compressed Token Operations (`compressed_token/`) +Operations on compressed accounts stored in Merkle trees. + +- **`mint_action/`** - MintAction instruction for compressed mint management + - `processor.rs` - Main instruction processor + - `accounts.rs` - Account validation and parsing + - `actions/` - Individual action handlers (create_mint, mint_to, decompress_mint, etc.) - **`transfer2/`** - Unified transfer instruction supporting multiple modes - `compression/` - Compress & decompress functionality - `ctoken/` - CToken-specific compression (compress_and_close.rs, decompress.rs, etc.) - `spl.rs` - SPL token compression - `processor.rs` - Main instruction processor - `accounts.rs` - Account validation and parsing -- **`mint_action/`** - Mint tokens to compressed/decompressed accounts -- **`ctoken_approve_revoke.rs`** - CTokenApprove (4), CTokenRevoke (5), CTokenApproveChecked (13) -- **`ctoken_mint_to.rs`** - CTokenMintTo (7), CTokenMintToChecked (14) -- **`ctoken_burn.rs`** - CTokenBurn (8), CTokenBurnChecked (15) -- **`ctoken_freeze_thaw.rs`** - CTokenFreezeAccount (10), CTokenThawAccount (11) -## Rent Management +## CToken Operations (`ctoken/`) +Operations on CToken Solana accounts (decompressed compressed tokens). + +- **`create.rs`** - Create regular ctoken accounts with optional compressible extension +- **`create_ata.rs`** - Create deterministic ATA accounts +- **`close/`** - Close ctoken accounts, handle rent distribution +- **`transfer/`** - SPL-compatible transfers between decompressed accounts + - `default.rs` - CTokenTransfer (discriminator: 3) + - `checked.rs` - CTokenTransferChecked (discriminator: 12) + - `shared.rs` - Common transfer utilities +- **`approve_revoke.rs`** - CTokenApprove (4), CTokenRevoke (5), CTokenApproveChecked (13) +- **`mint_to.rs`** - CTokenMintTo (7), CTokenMintToChecked (14) +- **`burn.rs`** - CTokenBurn (8), CTokenBurnChecked (15) +- **`freeze_thaw.rs`** - CTokenFreezeAccount (10), CTokenThawAccount (11) + +## Rent Management (`compressible/`) - **`claim.rs`** - Claim rent from expired compressible accounts - **`withdraw_funding_pool.rs`** - Withdraw funds from rent recipient pool diff --git a/programs/compressed-token/program/docs/CLAUDE.md b/programs/compressed-token/program/docs/CLAUDE.md index 461b7bf356..3f328b6b7c 100644 --- a/programs/compressed-token/program/docs/CLAUDE.md +++ b/programs/compressed-token/program/docs/CLAUDE.md @@ -8,30 +8,36 @@ This documentation is organized to provide clear navigation through the compress - **`../CLAUDE.md`** (parent) - Main entry point with summary and instruction index - **`ACCOUNTS.md`** - Complete account layouts and data structures - **`EXTENSIONS.md`** - Token-2022 extension validation across ctoken instructions +- **`INSTRUCTIONS.md`** - Full instruction reference and discriminator table - **`RESTRICTED_T22_EXTENSIONS.md`** - SPL Token-2022 behavior for 5 restricted extensions - **`T22_VS_CTOKEN_COMPARISON.md`** - Comparison of T22 vs ctoken extension behavior -- **`instructions/`** - Detailed instruction documentation - - `CREATE_TOKEN_ACCOUNT.md` - Create token account & associated token account instructions +- **`compressed_token/`** - Compressed token operations (Merkle tree accounts) + - `TRANSFER2.md` - Batch transfer with compress/decompress operations - `MINT_ACTION.md` - Mint operations and compressed mint management - - `TRANSFER2.md` - Batch transfer instruction for compressed/decompressed operations - - `CLAIM.md` - Claim rent from expired compressible accounts - - `CLOSE_TOKEN_ACCOUNT.md` - Close decompressed token accounts - - `CTOKEN_TRANSFER.md` - Transfer between decompressed accounts - - `CTOKEN_TRANSFER_CHECKED.md` - Transfer with decimals validation - - `CTOKEN_APPROVE.md` - Approve delegate on decompressed CToken account - - `CTOKEN_REVOKE.md` - Revoke delegate on decompressed CToken account - - `CTOKEN_MINT_TO.md` - Mint tokens to decompressed CToken account - - `CTOKEN_BURN.md` - Burn tokens from decompressed CToken account - - `CTOKEN_FREEZE_ACCOUNT.md` - Freeze decompressed CToken account - - `CTOKEN_THAW_ACCOUNT.md` - Thaw frozen decompressed CToken account - - `CTOKEN_APPROVE_CHECKED.md` - Approve delegate with decimals validation - - `CTOKEN_MINT_TO_CHECKED.md` - Mint tokens with decimals validation - - `CTOKEN_BURN_CHECKED.md` - Burn tokens with decimals validation - - `WITHDRAW_FUNDING_POOL.md` - Withdraw funds from rent recipient pool + - `FREEZE.md` - Freeze compressed token accounts (Anchor) + - `THAW.md` - Thaw frozen compressed token accounts (Anchor) - `CREATE_TOKEN_POOL.md` - Create initial token pool for SPL/T22 mint compression - `ADD_TOKEN_POOL.md` - Add additional token pools (up to 5 per mint) +- **`compressible/`** - Rent management for compressible accounts + - `CLAIM.md` - Claim rent from expired compressible accounts + - `WITHDRAW_FUNDING_POOL.md` - Withdraw funds from rent recipient pool +- **`ctoken/`** - CToken (decompressed) account operations + - `CREATE.md` - Create token account & associated token account + - `CLOSE.md` - Close decompressed token accounts + - `TRANSFER.md` - Transfer between decompressed accounts + - `TRANSFER_CHECKED.md` - Transfer with decimals validation + - `APPROVE.md` - Approve delegate + - `APPROVE_CHECKED.md` - Approve with decimals validation + - `REVOKE.md` - Revoke delegate + - `MINT_TO.md` - Mint tokens to CToken account + - `MINT_TO_CHECKED.md` - Mint with decimals validation + - `BURN.md` - Burn tokens from CToken account + - `BURN_CHECKED.md` - Burn with decimals validation + - `FREEZE_ACCOUNT.md` - Freeze CToken account + - `THAW_ACCOUNT.md` - Thaw frozen CToken account ## Navigation Tips - Start with `../CLAUDE.md` for the instruction index and overview - Use `ACCOUNTS.md` for account structure reference +- Use `INSTRUCTIONS.md` for discriminator reference and instruction index - Refer to specific instruction docs for implementation details diff --git a/programs/compressed-token/program/docs/EXTENSIONS.md b/programs/compressed-token/program/docs/EXTENSIONS.md index cf70935947..d2c39d30ec 100644 --- a/programs/compressed-token/program/docs/EXTENSIONS.md +++ b/programs/compressed-token/program/docs/EXTENSIONS.md @@ -136,7 +136,7 @@ The compressed token program supports 16 Token-2022 extension types. **5 restric **Validation paths:** - `programs/compressed-token/program/src/extensions/check_mint_extensions.rs:77-84` - Extracts delegate pubkey in `parse_mint_extensions()` - `programs/compressed-token/program/src/shared/owner_validation.rs:30-78` - `verify_owner_or_delegate_signer()` validates delegate/permanent delegate signer -- `programs/compressed-token/program/src/transfer/shared.rs:164-179` - `validate_permanent_delegate()` +- `programs/compressed-token/program/src/ctoken/transfer/shared.rs:164-179` - `validate_permanent_delegate()` **Unchecked instructions:** 1. CTokenApprove @@ -247,7 +247,7 @@ pub struct CompressedOnlyExtensionInstructionData { ### When Created (CompressAndClose) -**Path:** `programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs` +**Path:** `programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs` **Trigger:** `ZCompressionMode::CompressAndClose` with `compression_only=true` on source CToken account. @@ -274,7 +274,7 @@ ctoken.base.set_initialized(); ### When Consumed (Decompress) -**Path:** `programs/compressed-token/program/src/transfer2/compression/ctoken/decompress.rs` +**Path:** `programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/decompress.rs` **Trigger:** Decompressing a compressed token that has CompressedOnly extension. @@ -409,7 +409,7 @@ MintExtensionChecks { --- ### `build_mint_extension_cache()` -**Path:** `programs/compressed-token/program/src/transfer2/check_extensions.rs:77-145` +**Path:** `programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs:77-145` **Used by:** Transfer2 (batch validation) @@ -458,7 +458,7 @@ MintExtensionChecks { - CompressAndClose still requires CompressedOnly output extension for restricted mints (lines 116-137) - If missing → `CompressAndCloseMissingCompressedOnlyExtension` (6133) -**Path:** `programs/compressed-token/program/src/transfer2/processor.rs:61` calls `build_mint_extension_cache()` +**Path:** `programs/compressed-token/program/src/compressed_token/transfer2/processor.rs:61` calls `build_mint_extension_cache()` ### Anchor Instructions diff --git a/programs/compressed-token/program/docs/instructions/CLAUDE.md b/programs/compressed-token/program/docs/INSTRUCTIONS.md similarity index 52% rename from programs/compressed-token/program/docs/instructions/CLAUDE.md rename to programs/compressed-token/program/docs/INSTRUCTIONS.md index 31bc2c2d9c..5e769a590b 100644 --- a/programs/compressed-token/program/docs/instructions/CLAUDE.md +++ b/programs/compressed-token/program/docs/INSTRUCTIONS.md @@ -8,28 +8,30 @@ This documentation is organized to provide clear navigation through the compress - **`../CLAUDE.md`** (parent) - Main entry point with summary and instruction index - **`ACCOUNTS.md`** - Complete account layouts and data structures - **`instructions/`** - Detailed instruction documentation - - `CREATE_TOKEN_ACCOUNT.md` - Create token account & associated token account instructions - - `MINT_ACTION.md` - Mint operations and compressed mint management - - `TRANSFER2.md` - Batch transfer instruction for compressed/decompressed operations - - `CLAIM.md` - Claim rent from expired compressible accounts - - `CLOSE_TOKEN_ACCOUNT.md` - Close decompressed token accounts - - `CTOKEN_TRANSFER.md` - Transfer between decompressed accounts - - `CTOKEN_TRANSFER_CHECKED.md` - Transfer with decimals validation - - `WITHDRAW_FUNDING_POOL.md` - Withdraw funds from rent recipient pool + - **`compressed_token/`** - Compressed token operations (Merkle tree accounts) + - `TRANSFER2.md` - Batch transfer with compress/decompress operations + - `MINT_ACTION.md` - Mint operations and compressed mint management + - `FREEZE.md` - Freeze compressed token accounts (Anchor) + - `THAW.md` - Thaw frozen compressed token accounts (Anchor) + - **`compressible/`** - Rent management for compressible accounts + - `CLAIM.md` - Claim rent from expired compressible accounts + - `WITHDRAW_FUNDING_POOL.md` - Withdraw funds from rent recipient pool + - **`ctoken/`** - CToken (decompressed) account operations + - `CREATE.md` - Create token account & associated token account + - `CLOSE.md` - Close decompressed token accounts + - `TRANSFER.md` - Transfer between decompressed accounts + - `TRANSFER_CHECKED.md` - Transfer with decimals validation + - `APPROVE.md` - Approve delegate + - `APPROVE_CHECKED.md` - Approve with decimals validation + - `REVOKE.md` - Revoke delegate + - `MINT_TO.md` - Mint tokens to CToken account + - `MINT_TO_CHECKED.md` - Mint with decimals validation + - `BURN.md` - Burn tokens from CToken account + - `BURN_CHECKED.md` - Burn with decimals validation + - `FREEZE_ACCOUNT.md` - Freeze CToken account + - `THAW_ACCOUNT.md` - Thaw frozen CToken account - `CREATE_TOKEN_POOL.md` - Create initial token pool for SPL/T22 mint compression - `ADD_TOKEN_POOL.md` - Add additional token pools (up to 5 per mint) - - `CTOKEN_APPROVE.md` - Approve delegate on decompressed CToken account - - `CTOKEN_REVOKE.md` - Revoke delegate on decompressed CToken account - - `CTOKEN_MINT_TO.md` - Mint tokens to decompressed CToken account - - `CTOKEN_BURN.md` - Burn tokens from decompressed CToken account - - `CTOKEN_FREEZE_ACCOUNT.md` - Freeze decompressed CToken account - - `CTOKEN_THAW_ACCOUNT.md` - Thaw frozen decompressed CToken account - - `CTOKEN_APPROVE_CHECKED.md` - Approve delegate with decimals validation - - `CTOKEN_MINT_TO_CHECKED.md` - Mint tokens with decimals validation - - `CTOKEN_BURN_CHECKED.md` - Burn tokens with decimals validation - - `compressed_token/` - Anchor program instructions for compressed token accounts - - `FREEZE.md` - Freeze compressed token accounts - - `THAW.md` - Thaw frozen compressed token accounts ## Discriminator Reference @@ -79,24 +81,26 @@ every instruction description must include the sections: - **instruciton logic and checks** - **Errors** possible errors and description what causes these errors -1. **Create Token Account Instructions** - Create regular and associated ctoken accounts -2. **Transfer2** - Batch transfer instruction supporting compress/decompress/transfer operations -3. **MintAction** - Batch instruction for compressed mint management and mint operations (supports 9 actions: CreateCompressedMint, MintTo, UpdateMintAuthority, UpdateFreezeAuthority, CreateSplMint, MintToCToken, UpdateMetadataField, UpdateMetadataAuthority, RemoveMetadataKey) -4. **Claim** - Rent reclamation from expired compressible accounts -5. **Close Token Account** - Close decompressed token accounts with rent distribution -6. **Decompressed Transfer** - SPL-compatible transfers between decompressed accounts -7. **Withdraw Funding Pool** - Withdraw funds from rent recipient pool -8. **Create Token Pool** - Create initial token pool PDA for SPL/T22 mint compression -9. **Add Token Pool** - Add additional token pools for a mint (up to 5 per mint) -10. **CToken MintTo** - Mint tokens to decompressed CToken account -11. **CToken Burn** - Burn tokens from decompressed CToken account -12. **CToken Freeze/Thaw** - Freeze and thaw decompressed CToken accounts -13. **CToken Approve/Revoke** - Approve and revoke delegate on decompressed CToken accounts -14. **CToken Checked Operations** - ApproveChecked, MintToChecked, BurnChecked with decimals validation +## Compressed Token Operations (`compressed_token/`) +1. **Transfer2** - Batch transfer instruction supporting compress/decompress/transfer operations +2. **MintAction** - Batch instruction for compressed mint management (9 actions) +3. **Freeze** - Freeze compressed token accounts (Anchor) +4. **Thaw** - Thaw frozen compressed token accounts (Anchor) -## Anchor Program Instructions (Compressed Token Accounts) +## CToken Operations (`ctoken/`) +5. **Create** - Create regular and associated ctoken accounts +6. **Close** - Close decompressed token accounts with rent distribution +7. **Transfer** - SPL-compatible transfers between decompressed accounts +8. **Approve/Revoke** - Approve and revoke delegate on decompressed CToken accounts +9. **MintTo** - Mint tokens to decompressed CToken account +10. **Burn** - Burn tokens from decompressed CToken account +11. **Freeze/Thaw** - Freeze and thaw decompressed CToken accounts +12. **Checked Operations** - TransferChecked, ApproveChecked, MintToChecked, BurnChecked -These instructions operate on compressed token accounts (stored in Merkle trees) and require ZK proofs: +## Compressible Operations (`compressible/`) +13. **Claim** - Rent reclamation from expired compressible accounts +14. **Withdraw Funding Pool** - Withdraw funds from rent recipient pool -15. **Compressed Token Freeze** (`compressed_token/FREEZE.md`) - Freeze compressed token accounts -16. **Compressed Token Thaw** (`compressed_token/THAW.md`) - Thaw frozen compressed token accounts +## Token Pool Operations (root) +15. **Create Token Pool** - Create initial token pool PDA for SPL/T22 mint compression +16. **Add Token Pool** - Add additional token pools for a mint (up to 5 per mint) diff --git a/programs/compressed-token/program/docs/instructions/ADD_TOKEN_POOL.md b/programs/compressed-token/program/docs/compressed_token/ADD_TOKEN_POOL.md similarity index 100% rename from programs/compressed-token/program/docs/instructions/ADD_TOKEN_POOL.md rename to programs/compressed-token/program/docs/compressed_token/ADD_TOKEN_POOL.md diff --git a/programs/compressed-token/program/docs/instructions/CREATE_TOKEN_POOL.md b/programs/compressed-token/program/docs/compressed_token/CREATE_TOKEN_POOL.md similarity index 100% rename from programs/compressed-token/program/docs/instructions/CREATE_TOKEN_POOL.md rename to programs/compressed-token/program/docs/compressed_token/CREATE_TOKEN_POOL.md diff --git a/programs/compressed-token/program/docs/instructions/compressed_token/FREEZE.md b/programs/compressed-token/program/docs/compressed_token/FREEZE.md similarity index 100% rename from programs/compressed-token/program/docs/instructions/compressed_token/FREEZE.md rename to programs/compressed-token/program/docs/compressed_token/FREEZE.md diff --git a/programs/compressed-token/program/docs/instructions/MINT_ACTION.md b/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md similarity index 99% rename from programs/compressed-token/program/docs/instructions/MINT_ACTION.md rename to programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md index 8157914f2a..e26737a365 100644 --- a/programs/compressed-token/program/docs/instructions/MINT_ACTION.md +++ b/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md @@ -2,7 +2,7 @@ **discriminator:** 103 **enum:** `InstructionType::MintAction` -**path:** programs/compressed-token/program/src/mint_action/ +**path:** programs/compressed-token/program/src/compressed_token/mint_action/ **description:** Batch instruction for managing compressed mint accounts (cmints) and performing mint operations. A compressed mint account stores the mint's supply, decimals, authorities (mint/freeze), and optional TokenMetadata extension in compressed state. TokenMetadata is the only extension supported for compressed mints and provides fields for name, symbol, uri, update_authority, and additional key-value metadata. diff --git a/programs/compressed-token/program/docs/instructions/compressed_token/THAW.md b/programs/compressed-token/program/docs/compressed_token/THAW.md similarity index 100% rename from programs/compressed-token/program/docs/instructions/compressed_token/THAW.md rename to programs/compressed-token/program/docs/compressed_token/THAW.md diff --git a/programs/compressed-token/program/docs/instructions/TRANSFER2.md b/programs/compressed-token/program/docs/compressed_token/TRANSFER2.md similarity index 99% rename from programs/compressed-token/program/docs/instructions/TRANSFER2.md rename to programs/compressed-token/program/docs/compressed_token/TRANSFER2.md index 1bf886d016..56a15f97d2 100644 --- a/programs/compressed-token/program/docs/instructions/TRANSFER2.md +++ b/programs/compressed-token/program/docs/compressed_token/TRANSFER2.md @@ -14,7 +14,7 @@ **discriminator:** 101 **enum:** `InstructionType::Transfer2` -**path:** programs/compressed-token/program/src/transfer2/ +**path:** programs/compressed-token/program/src/compressed_token/transfer2/ **description:** 1. Batch transfer instruction supporting multiple token operations in a single transaction with up to 5 different mints (cmints or spl) diff --git a/programs/compressed-token/program/docs/instructions/CLAIM.md b/programs/compressed-token/program/docs/compressible/CLAIM.md similarity index 98% rename from programs/compressed-token/program/docs/instructions/CLAIM.md rename to programs/compressed-token/program/docs/compressible/CLAIM.md index d49af25124..c642b2fa4d 100644 --- a/programs/compressed-token/program/docs/instructions/CLAIM.md +++ b/programs/compressed-token/program/docs/compressible/CLAIM.md @@ -2,7 +2,7 @@ **discriminator:** 104 **enum:** `InstructionType::Claim` -**path:** programs/compressed-token/program/src/claim.rs +**path:** programs/compressed-token/program/src/compressible/claim.rs **description:** 1. Claims rent from compressible CToken and CMint solana accounts that have passed their rent expiration epochs diff --git a/programs/compressed-token/program/docs/instructions/WITHDRAW_FUNDING_POOL.md b/programs/compressed-token/program/docs/compressible/WITHDRAW_FUNDING_POOL.md similarity index 97% rename from programs/compressed-token/program/docs/instructions/WITHDRAW_FUNDING_POOL.md rename to programs/compressed-token/program/docs/compressible/WITHDRAW_FUNDING_POOL.md index f13a03b0b3..b87d71d186 100644 --- a/programs/compressed-token/program/docs/instructions/WITHDRAW_FUNDING_POOL.md +++ b/programs/compressed-token/program/docs/compressible/WITHDRAW_FUNDING_POOL.md @@ -2,7 +2,7 @@ **discriminator:** 105 **enum:** `InstructionType::WithdrawFundingPool` -**path:** programs/compressed-token/program/src/withdraw_funding_pool.rs +**path:** programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs **description:** 1. Withdraws lamports from the rent_sponsor PDA pool to a specified destination account diff --git a/programs/compressed-token/program/docs/instructions/CTOKEN_APPROVE.md b/programs/compressed-token/program/docs/ctoken/APPROVE.md similarity index 98% rename from programs/compressed-token/program/docs/instructions/CTOKEN_APPROVE.md rename to programs/compressed-token/program/docs/ctoken/APPROVE.md index 8fcac00f4f..dfeadfe66b 100644 --- a/programs/compressed-token/program/docs/instructions/CTOKEN_APPROVE.md +++ b/programs/compressed-token/program/docs/ctoken/APPROVE.md @@ -2,7 +2,7 @@ **discriminator:** 4 **enum:** `InstructionType::CTokenApprove` -**path:** programs/compressed-token/program/src/ctoken_approve_revoke.rs +**path:** programs/compressed-token/program/src/ctoken/approve_revoke.rs ### SPL Instruction Format Compatibility This instruction is compatible with the SPL Token instruction format (using `spl_token_2022::instruction::approve` with changed program ID) when **no top-up is required**. @@ -17,7 +17,7 @@ If the CToken account has a compressible extension and requires a rent top-up, t Delegates a specified amount to a delegate authority on a decompressed ctoken account (account layout `CToken` defined in program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs). Before the approve operation, automatically tops up compressible accounts (extension layout `CompressionInfo` defined in program-libs/compressible/src/compression_info.rs) with additional lamports if needed to prevent accounts from becoming compressible during normal operations. The instruction supports a max_top_up parameter (0 = no limit) that enforces transaction failure if the calculated top-up exceeds this limit. Uses pinocchio-token-program for SPL-compatible approve semantics. Supports backwards-compatible instruction data format (8 bytes legacy vs 10 bytes with max_top_up). **Instruction data:** -Path: programs/compressed-token/program/src/ctoken_approve_revoke.rs (lines 34-66) +Path: programs/compressed-token/program/src/ctoken/approve_revoke.rs (lines 34-66) - Bytes 0-7: `amount` (u64, little-endian) - Number of tokens to delegate - Bytes 8-9 (optional): `max_top_up` (u16, little-endian) - Maximum lamports for top-up (0 = no limit, default for legacy format) diff --git a/programs/compressed-token/program/docs/instructions/CTOKEN_APPROVE_CHECKED.md b/programs/compressed-token/program/docs/ctoken/APPROVE_CHECKED.md similarity index 98% rename from programs/compressed-token/program/docs/instructions/CTOKEN_APPROVE_CHECKED.md rename to programs/compressed-token/program/docs/ctoken/APPROVE_CHECKED.md index c6ed24fe45..54ff45369a 100644 --- a/programs/compressed-token/program/docs/instructions/CTOKEN_APPROVE_CHECKED.md +++ b/programs/compressed-token/program/docs/ctoken/APPROVE_CHECKED.md @@ -2,13 +2,13 @@ **discriminator:** 13 **enum:** `InstructionType::CTokenApproveChecked` -**path:** programs/compressed-token/program/src/ctoken_approve_revoke.rs +**path:** programs/compressed-token/program/src/ctoken/approve_revoke.rs **description:** Delegates a specified amount to a delegate authority on a decompressed ctoken account with decimals validation, fully compatible with SPL Token ApproveChecked semantics. Account layout `CToken` is defined in program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs. Extension layout `CompressionInfo` is defined in program-libs/compressible/src/compression_info.rs. Uses pinocchio-token-program to process the approve operation. Before the approve operation, automatically tops up compressible accounts with additional lamports if needed to prevent accounts from becoming compressible during normal operations. Supports max_top_up parameter (0 = no limit) that enforces transaction failure if the calculated top-up exceeds this limit. Uses cached decimals optimization: if source CToken has cached decimals, validates against instruction decimals and skips mint read. Cached decimals allow users to choose whether a cmint is required to be decompressed at account creation or transfer. **Instruction data:** -Path: programs/compressed-token/program/src/ctoken_approve_revoke.rs (lines 163-217) +Path: programs/compressed-token/program/src/ctoken/approve_revoke.rs (lines 163-217) - Bytes 0-7: `amount` (u64, little-endian) - Number of tokens to delegate - Byte 8: `decimals` (u8) - Expected token decimals diff --git a/programs/compressed-token/program/docs/instructions/CTOKEN_BURN.md b/programs/compressed-token/program/docs/ctoken/BURN.md similarity index 99% rename from programs/compressed-token/program/docs/instructions/CTOKEN_BURN.md rename to programs/compressed-token/program/docs/ctoken/BURN.md index 86cf799197..43d168ed1b 100644 --- a/programs/compressed-token/program/docs/instructions/CTOKEN_BURN.md +++ b/programs/compressed-token/program/docs/ctoken/BURN.md @@ -2,7 +2,7 @@ **discriminator:** 8 **enum:** `InstructionType::CTokenBurn` -**path:** programs/compressed-token/program/src/ctoken_burn.rs +**path:** programs/compressed-token/program/src/ctoken/burn.rs **description:** Burns tokens from a decompressed CToken account and decreases the CMint supply, fully compatible with SPL Token burn semantics. Account layout `CToken` is defined in `program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs`. Account layout `CompressedMint` (CMint) is defined in `program-libs/ctoken-interface/src/state/mint/compressed_mint.rs`. Extension layout `CompressionInfo` is defined in `program-libs/compressible/src/compression_info.rs` and is embedded in both CToken and CMint structs. Uses pinocchio-token-program to process the burn (handles balance/supply updates, authority check, frozen check). After the burn, automatically tops up compressible accounts with additional lamports if needed. Top-up is calculated for both CMint and source CToken based on current slot and account balance. Top-up prevents accounts from becoming compressible during normal operations. Enforces max_top_up limit if provided (transaction fails if exceeded). Supports max_top_up parameter to limit rent top-up costs (0 = no limit). Instruction data is backwards-compatible: 8-byte format (legacy, no max_top_up enforcement) and 10-byte format (with max_top_up). This instruction only works with CMints (compressed mints). CMints do not support restricted Token-2022 extensions (Pausable, TransferFee, TransferHook, PermanentDelegate, DefaultAccountState) - only TokenMetadata is allowed. To burn tokens from spl or T22 mints, use Transfer2 with decompress mode to convert to SPL tokens first, then burn via SPL Token-2022. diff --git a/programs/compressed-token/program/docs/instructions/CTOKEN_BURN_CHECKED.md b/programs/compressed-token/program/docs/ctoken/BURN_CHECKED.md similarity index 99% rename from programs/compressed-token/program/docs/instructions/CTOKEN_BURN_CHECKED.md rename to programs/compressed-token/program/docs/ctoken/BURN_CHECKED.md index bfb0712561..ff170ea7be 100644 --- a/programs/compressed-token/program/docs/instructions/CTOKEN_BURN_CHECKED.md +++ b/programs/compressed-token/program/docs/ctoken/BURN_CHECKED.md @@ -2,7 +2,7 @@ **discriminator:** 15 **enum:** `InstructionType::CTokenBurnChecked` -**path:** programs/compressed-token/program/src/ctoken_burn.rs +**path:** programs/compressed-token/program/src/ctoken/burn.rs **description:** Burns tokens from a decompressed CToken account and decreases the CMint supply with decimals validation, fully compatible with SPL Token BurnChecked semantics. Account layout `CToken` is defined in `program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs`. Account layout `CompressedMint` (CMint) is defined in `program-libs/ctoken-interface/src/state/mint/compressed_mint.rs`. Extension layout `CompressionInfo` is defined in `program-libs/compressible/src/compression_info.rs` and is embedded in both CToken and CMint structs. Uses pinocchio-token-program to process the burn_checked (handles balance/supply updates, authority check, frozen check, decimals validation). After the burn, automatically tops up compressible accounts with additional lamports if needed. Top-up prevents accounts from becoming compressible during normal operations. Enforces max_top_up limit if provided (transaction fails if exceeded). Account order is REVERSED from mint_to instruction: [source_ctoken, cmint, authority] vs mint_to's [cmint, destination_ctoken, authority]. diff --git a/programs/compressed-token/program/docs/instructions/CLOSE_TOKEN_ACCOUNT.md b/programs/compressed-token/program/docs/ctoken/CLOSE.md similarity index 99% rename from programs/compressed-token/program/docs/instructions/CLOSE_TOKEN_ACCOUNT.md rename to programs/compressed-token/program/docs/ctoken/CLOSE.md index be6e2fa198..9836104503 100644 --- a/programs/compressed-token/program/docs/instructions/CLOSE_TOKEN_ACCOUNT.md +++ b/programs/compressed-token/program/docs/ctoken/CLOSE.md @@ -2,7 +2,7 @@ **discriminator:** 9 **enum:** `CTokenInstruction::CloseTokenAccount` -**path:** programs/compressed-token/program/src/close_token_account/ +**path:** programs/compressed-token/program/src/ctoken/close/ **description:** 1. Closes decompressed ctoken solana accounts and distributes remaining lamports to destination account. diff --git a/programs/compressed-token/program/docs/instructions/CREATE_TOKEN_ACCOUNT.md b/programs/compressed-token/program/docs/ctoken/CREATE.md similarity index 98% rename from programs/compressed-token/program/docs/instructions/CREATE_TOKEN_ACCOUNT.md rename to programs/compressed-token/program/docs/ctoken/CREATE.md index 4713c8c626..1f68f51fa2 100644 --- a/programs/compressed-token/program/docs/instructions/CREATE_TOKEN_ACCOUNT.md +++ b/programs/compressed-token/program/docs/ctoken/CREATE.md @@ -15,7 +15,7 @@ **discriminator:** 18 **enum:** `CTokenInstruction::CreateTokenAccount` - **path:** programs/compressed-token/program/src/create_token_account.rs + **path:** programs/compressed-token/program/src/ctoken/create.rs **description:** 1. creates ctoken solana accounts with and without Compressible extension @@ -131,7 +131,7 @@ **discriminator:** 100 (non-idempotent), 102 (idempotent) **enum:** `CTokenInstruction::CreateAssociatedCTokenAccount` (non-idempotent), `CTokenInstruction::CreateAssociatedTokenAccountIdempotent` (idempotent) - **path:** programs/compressed-token/program/src/create_associated_token_account.rs + **path:** programs/compressed-token/program/src/ctoken/create_ata.rs **description:** 1. Creates deterministic ctoken PDA accounts derived from [owner, ctoken_program_id, mint] diff --git a/programs/compressed-token/program/docs/instructions/CTOKEN_FREEZE_ACCOUNT.md b/programs/compressed-token/program/docs/ctoken/FREEZE_ACCOUNT.md similarity index 98% rename from programs/compressed-token/program/docs/instructions/CTOKEN_FREEZE_ACCOUNT.md rename to programs/compressed-token/program/docs/ctoken/FREEZE_ACCOUNT.md index c71e90bb2c..9e4556bde0 100644 --- a/programs/compressed-token/program/docs/instructions/CTOKEN_FREEZE_ACCOUNT.md +++ b/programs/compressed-token/program/docs/ctoken/FREEZE_ACCOUNT.md @@ -2,7 +2,7 @@ **discriminator:** 10 **enum:** `InstructionType::CTokenFreezeAccount` -**path:** programs/compressed-token/program/src/ctoken_freeze_thaw.rs +**path:** programs/compressed-token/program/src/ctoken/freeze_thaw.rs **description:** Freezes a decompressed ctoken account, preventing transfers and other operations while frozen. This is a pass-through instruction that validates mint ownership (must be owned by SPL Token, Token-2022, or CToken program) before delegating to pinocchio-token-program for standard SPL Token freeze validation. After freezing, the account's state field is set to AccountState::Frozen, and only the freeze_authority of the mint can freeze accounts (mint must have freeze_authority set). The account layout `CToken` is defined in program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs. diff --git a/programs/compressed-token/program/docs/instructions/CTOKEN_MINT_TO.md b/programs/compressed-token/program/docs/ctoken/MINT_TO.md similarity index 98% rename from programs/compressed-token/program/docs/instructions/CTOKEN_MINT_TO.md rename to programs/compressed-token/program/docs/ctoken/MINT_TO.md index 642b2154ca..e7767a26d3 100644 --- a/programs/compressed-token/program/docs/instructions/CTOKEN_MINT_TO.md +++ b/programs/compressed-token/program/docs/ctoken/MINT_TO.md @@ -2,7 +2,7 @@ **discriminator:** 7 **enum:** `InstructionType::CTokenMintTo` -**path:** programs/compressed-token/program/src/ctoken_mint_to.rs +**path:** programs/compressed-token/program/src/ctoken/mint_to.rs **description:** Mints tokens from a decompressed CMint account to a destination CToken account, fully compatible with SPL Token mint_to semantics. Uses pinocchio-token-program to process the mint_to operation which handles balance/supply updates, authority validation, and frozen account checks. After minting, automatically tops up compressible accounts with additional lamports if needed to prevent accounts from becoming compressible during normal operations. Both CMint and destination CToken can receive top-ups based on their current slot and account balance. Supports max_top_up parameter to limit rent top-up costs where 0 means no limit. Instruction data is backwards-compatible with two formats: 8-byte format for legacy compatibility without max_top_up enforcement and 10-byte format with max_top_up. This instruction only works with CMints (compressed mints). CMints do not support restricted Token-2022 extensions (Pausable, TransferFee, TransferHook, PermanentDelegate, DefaultAccountState) - only TokenMetadata is allowed. @@ -13,7 +13,7 @@ Account layouts: - `CompressionInfo` extension defined in: program-libs/compressible/src/compression_info.rs **Instruction data:** -Path: programs/compressed-token/program/src/ctoken_mint_to.rs (lines 10-47) +Path: programs/compressed-token/program/src/ctoken/mint_to.rs (lines 10-47) Byte layout: - Bytes 0-7: `amount` (u64, little-endian) - Number of tokens to mint diff --git a/programs/compressed-token/program/docs/instructions/CTOKEN_MINT_TO_CHECKED.md b/programs/compressed-token/program/docs/ctoken/MINT_TO_CHECKED.md similarity index 97% rename from programs/compressed-token/program/docs/instructions/CTOKEN_MINT_TO_CHECKED.md rename to programs/compressed-token/program/docs/ctoken/MINT_TO_CHECKED.md index 08dc938c9e..07ef6f261e 100644 --- a/programs/compressed-token/program/docs/instructions/CTOKEN_MINT_TO_CHECKED.md +++ b/programs/compressed-token/program/docs/ctoken/MINT_TO_CHECKED.md @@ -2,7 +2,7 @@ **discriminator:** 14 **enum:** `InstructionType::CTokenMintToChecked` -**path:** programs/compressed-token/program/src/ctoken_mint_to.rs +**path:** programs/compressed-token/program/src/ctoken/mint_to.rs **description:** Mints tokens from a decompressed CMint account to a destination CToken account with decimals validation, fully compatible with SPL Token MintToChecked semantics. Uses pinocchio-token-program to process the mint_to_checked operation which handles balance/supply updates, authority validation, frozen account checks, and decimals validation. After minting, automatically tops up compressible accounts with additional lamports if needed to prevent accounts from becoming compressible during normal operations. Both CMint and destination CToken can receive top-ups based on their current slot and account balance. Supports max_top_up parameter to limit rent top-up costs where 0 means no limit. @@ -13,7 +13,7 @@ Account layouts: - `CompressionInfo` extension defined in: program-libs/compressible/src/compression_info.rs **Instruction data:** -Path: programs/compressed-token/program/src/ctoken_mint_to.rs (lines 62-112, function `process_ctoken_mint_to_checked`) +Path: programs/compressed-token/program/src/ctoken/mint_to.rs (lines 62-112, function `process_ctoken_mint_to_checked`) Byte layout: - Bytes 0-7: `amount` (u64, little-endian) - Number of tokens to mint diff --git a/programs/compressed-token/program/docs/instructions/CTOKEN_REVOKE.md b/programs/compressed-token/program/docs/ctoken/REVOKE.md similarity index 97% rename from programs/compressed-token/program/docs/instructions/CTOKEN_REVOKE.md rename to programs/compressed-token/program/docs/ctoken/REVOKE.md index da057bae30..e5fcd390ba 100644 --- a/programs/compressed-token/program/docs/instructions/CTOKEN_REVOKE.md +++ b/programs/compressed-token/program/docs/ctoken/REVOKE.md @@ -2,7 +2,7 @@ **discriminator:** 5 **enum:** `InstructionType::CTokenRevoke` -**path:** programs/compressed-token/program/src/ctoken_approve_revoke.rs +**path:** programs/compressed-token/program/src/ctoken/approve_revoke.rs ### SPL Instruction Format Compatibility @@ -18,7 +18,7 @@ If the CToken account has a compressible extension and requires a rent top-up, t Revokes any previously granted delegation on a decompressed ctoken account (account layout `CToken` defined in program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs). Before the revoke operation, automatically tops up compressible accounts (extension layout `CompressionInfo` defined in program-libs/compressible/src/compression_info.rs) with additional lamports if needed to prevent accounts from becoming compressible during normal operations. The instruction supports a max_top_up parameter (0 = no limit) that enforces transaction failure if the calculated top-up exceeds this limit. Uses pinocchio-token-program for SPL-compatible revoke semantics. Supports backwards-compatible instruction data format (0 bytes legacy vs 2 bytes with max_top_up). The revoke operation follows SPL Token rules exactly (clears delegate and delegated_amount). **Instruction data:** -Path: programs/compressed-token/program/src/ctoken_approve_revoke.rs (lines 71-106) +Path: programs/compressed-token/program/src/ctoken/approve_revoke.rs (lines 71-106) - Empty (0 bytes): legacy format, no max_top_up enforcement (max_top_up = 0, no limit) - Bytes 0-1 (optional): `max_top_up` (u16, little-endian) - Maximum lamports for top-up (0 = no limit) diff --git a/programs/compressed-token/program/docs/instructions/CTOKEN_THAW_ACCOUNT.md b/programs/compressed-token/program/docs/ctoken/THAW_ACCOUNT.md similarity index 98% rename from programs/compressed-token/program/docs/instructions/CTOKEN_THAW_ACCOUNT.md rename to programs/compressed-token/program/docs/ctoken/THAW_ACCOUNT.md index e5c4386f0a..0c35639fe4 100644 --- a/programs/compressed-token/program/docs/instructions/CTOKEN_THAW_ACCOUNT.md +++ b/programs/compressed-token/program/docs/ctoken/THAW_ACCOUNT.md @@ -2,7 +2,7 @@ **discriminator:** 11 **enum:** `InstructionType::CTokenThawAccount` -**path:** programs/compressed-token/program/src/ctoken_freeze_thaw.rs +**path:** programs/compressed-token/program/src/ctoken/freeze_thaw.rs **description:** Thaws a frozen decompressed ctoken account, restoring normal operation. This is a pass-through instruction that validates mint ownership (must be owned by SPL Token, Token-2022, or CToken program) before delegating to pinocchio-token-program for standard SPL Token thaw validation. After thawing, the account's state field is set to AccountState::Initialized, and only the freeze_authority of the mint can thaw accounts (mint must have freeze_authority set). The account layout `CToken` is defined in program-libs/ctoken-interface/src/state/ctoken/ctoken_struct.rs. diff --git a/programs/compressed-token/program/docs/instructions/CTOKEN_TRANSFER.md b/programs/compressed-token/program/docs/ctoken/TRANSFER.md similarity index 98% rename from programs/compressed-token/program/docs/instructions/CTOKEN_TRANSFER.md rename to programs/compressed-token/program/docs/ctoken/TRANSFER.md index 5e4f95fc93..17e787aa71 100644 --- a/programs/compressed-token/program/docs/instructions/CTOKEN_TRANSFER.md +++ b/programs/compressed-token/program/docs/ctoken/TRANSFER.md @@ -2,7 +2,7 @@ **discriminator:** 3 **enum:** `InstructionType::CTokenTransfer` -**path:** programs/compressed-token/program/src/transfer/default.rs +**path:** programs/compressed-token/program/src/ctoken/transfer/default.rs ### SPL Instruction Format Compatibility diff --git a/programs/compressed-token/program/docs/instructions/CTOKEN_TRANSFER_CHECKED.md b/programs/compressed-token/program/docs/ctoken/TRANSFER_CHECKED.md similarity index 99% rename from programs/compressed-token/program/docs/instructions/CTOKEN_TRANSFER_CHECKED.md rename to programs/compressed-token/program/docs/ctoken/TRANSFER_CHECKED.md index e532e9f375..73046bd193 100644 --- a/programs/compressed-token/program/docs/instructions/CTOKEN_TRANSFER_CHECKED.md +++ b/programs/compressed-token/program/docs/ctoken/TRANSFER_CHECKED.md @@ -2,7 +2,7 @@ **discriminator:** 12 **enum:** `InstructionType::CTokenTransferChecked` -**path:** programs/compressed-token/program/src/transfer/checked.rs +**path:** programs/compressed-token/program/src/ctoken/transfer/checked.rs ### SPL Instruction Format Compatibility From 1917f521a691880166836cf63460ececaf97b386 Mon Sep 17 00:00:00 2001 From: ananas Date: Tue, 6 Jan 2026 00:31:38 +0000 Subject: [PATCH 04/18] fix: create ctoken account --- .../instructions/extensions/compressible.rs | 5 +- .../tests/ctoken/create.rs | 241 ++++++++++++ .../tests/ctoken/create_ata.rs | 197 ++++++++++ programs/compressed-token/anchor/src/lib.rs | 306 +++++++-------- .../program/docs/ctoken/CREATE.md | 63 ++- .../mint_action/actions/decompress_mint.rs | 2 +- .../program/src/compressible/claim.rs | 5 +- .../src/compressible/withdraw_funding_pool.rs | 2 +- .../program/src/ctoken/create.rs | 223 ++--------- .../program/src/ctoken/create_ata.rs | 91 +---- .../program/src/shared/config_account.rs | 44 +++ .../src/shared/initialize_ctoken_account.rs | 166 ++++++-- .../program/src/shared/mod.rs | 3 + .../program/tests/check_authority.rs | 2 +- .../program/tests/compress_and_close.rs | 2 +- .../compressed-token/program/tests/mint.rs | 4 +- .../program/tests/mint_action.rs | 2 +- .../program/tests/mint_validation.rs | 358 ++++++++++++++++++ .../program/tests/multi_sum_check.rs | 2 +- .../program/tests/queue_indices.rs | 2 +- 20 files changed, 1228 insertions(+), 492 deletions(-) create mode 100644 programs/compressed-token/program/src/shared/config_account.rs create mode 100644 programs/compressed-token/program/tests/mint_validation.rs diff --git a/program-libs/ctoken-interface/src/instructions/extensions/compressible.rs b/program-libs/ctoken-interface/src/instructions/extensions/compressible.rs index 1a6c92c67f..496006eb5a 100644 --- a/program-libs/ctoken-interface/src/instructions/extensions/compressible.rs +++ b/program-libs/ctoken-interface/src/instructions/extensions/compressible.rs @@ -18,8 +18,9 @@ pub struct CompressibleExtensionInstructionData { /// Rent payment in epochs. /// Paid once at initialization. pub rent_payment: u8, - /// Placeholder for future use. If true, the compressed token account cannot be transferred, - /// only decompressed. Currently unused - always set to 0. + /// If non-zero, the compressed token account cannot be transferred, only decompressed. + /// Required for mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook). + /// Must be set for compressible ATAs. pub compression_only: u8, pub write_top_up: u32, pub compress_to_account_pubkey: Option, diff --git a/program-tests/compressed-token-test/tests/ctoken/create.rs b/program-tests/compressed-token-test/tests/ctoken/create.rs index 19b5b180c4..6082cead8e 100644 --- a/program-tests/compressed-token-test/tests/ctoken/create.rs +++ b/program-tests/compressed-token-test/tests/ctoken/create.rs @@ -89,6 +89,59 @@ async fn test_create_compressible_token_account_instruction() { create_and_assert_token_account(&mut context, compressible_data, "No lamports_per_write") .await; } + + // Test 6: Maximum prepaid epochs (255) - boundary test + { + context.token_account_keypair = Keypair::new(); + let compressible_data = CompressibleData { + compression_authority: context.compression_authority, + rent_sponsor: payer_pubkey, // Use payer as rent sponsor for large epoch payment + num_prepaid_epochs: 255, // Maximum u8 value + lamports_per_write: Some(100), + account_version: light_ctoken_interface::state::TokenDataVersion::ShaFlat, + compress_to_pubkey: false, + payer: payer_pubkey, + }; + create_and_assert_token_account(&mut context, compressible_data, "max_prepaid_epochs_255") + .await; + } + + // Test 7: Exactly max_top_up for lamports_per_write - boundary test + { + context.token_account_keypair = Keypair::new(); + let max_top_up = RentConfig::default().max_top_up as u32; + let compressible_data = CompressibleData { + compression_authority: context.compression_authority, + rent_sponsor: context.rent_sponsor, + num_prepaid_epochs: 2, + lamports_per_write: Some(max_top_up), // Exactly at limit (should succeed) + account_version: light_ctoken_interface::state::TokenDataVersion::ShaFlat, + compress_to_pubkey: false, + payer: payer_pubkey, + }; + create_and_assert_token_account( + &mut context, + compressible_data, + "exactly_max_top_up_lamports_per_write", + ) + .await; + } + + // Test 8: Zero lamports_per_write - edge case + { + context.token_account_keypair = Keypair::new(); + let compressible_data = CompressibleData { + compression_authority: context.compression_authority, + rent_sponsor: context.rent_sponsor, + num_prepaid_epochs: 2, + lamports_per_write: Some(0), // Zero (should succeed) + account_version: light_ctoken_interface::state::TokenDataVersion::ShaFlat, + compress_to_pubkey: false, + payer: payer_pubkey, + }; + create_and_assert_token_account(&mut context, compressible_data, "zero_lamports_per_write") + .await; + } } #[tokio::test] @@ -453,4 +506,192 @@ async fn test_create_compressible_token_account_failing() { // Should fail with MissingRequiredSignature (8) light_program_test::utils::assert::assert_rpc_error(result, 0, 8).unwrap(); } + + // Test 10: Non-compressible account for mint with restricted extensions + // Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) + // require the compression_only marker which is part of the Compressible extension. + // Error: 6115 (MissingCompressibleConfig) + { + use forester_utils::instructions::create_account::create_account_instruction; + use light_test_utils::mint_2022::create_mint_22_with_extension_types; + use solana_sdk::instruction::{AccountMeta, Instruction}; + use spl_token_2022::extension::ExtensionType; + + println!("Test 10: Non-compressible account for mint with restricted extensions"); + + // Create a Token-2022 mint with TransferFeeConfig (a restricted extension) + let (mint_keypair, _config) = create_mint_22_with_extension_types( + &mut context.rpc, + &context.payer, + 9, // decimals + &[ExtensionType::TransferFeeConfig], + ) + .await; + let mint_with_restricted_ext = mint_keypair.pubkey(); + + // Pre-allocate 200-byte token account owned by ctoken program + let token_account_keypair = Keypair::new(); + let token_account_pubkey = token_account_keypair.pubkey(); + let account_size = 200usize; + + let create_account_ix = create_account_instruction( + &payer_pubkey, + account_size, + context + .rpc + .get_minimum_balance_for_rent_exemption(account_size) + .await + .unwrap(), + &light_compressed_token::ID, + Some(&token_account_keypair), + ); + + context + .rpc + .create_and_send_transaction( + &[create_account_ix], + &payer_pubkey, + &[&context.payer, &token_account_keypair], + ) + .await + .unwrap(); + + // Build manual instruction for non-compressible path: + // - discriminator: 18 (InitializeAccount3) + // - data: just 32 bytes (owner pubkey) - triggers non-compressible path + // - accounts: token_account (mutable), mint (non-mutable) + let owner_pubkey = context.owner_keypair.pubkey(); + let mut instruction_data = vec![18u8]; // discriminator + instruction_data.extend_from_slice(&owner_pubkey.to_bytes()); // 32-byte owner + + let create_non_compressible_ix = Instruction { + program_id: light_compressed_token::ID, + accounts: vec![ + AccountMeta::new(token_account_pubkey, false), // token_account (mutable, not signer) + AccountMeta::new_readonly(mint_with_restricted_ext, false), // mint + ], + data: instruction_data, + }; + + let result = context + .rpc + .create_and_send_transaction( + &[create_non_compressible_ix], + &payer_pubkey, + &[&context.payer], + ) + .await; + + // Should fail with MissingCompressibleConfig (6115) + // Rationale: Mints with restricted extensions must be marked as compression_only, + // and that marker is part of the Compressible extension + light_program_test::utils::assert::assert_rpc_error(result, 0, 6115).unwrap(); + } + + // Test 11: Token account passed as mint + // Passing a valid T22 token account instead of a mint should fail validation. + // The is_valid_mint function checks AccountType at offset 165 - token accounts have AccountType=2. + // Error: 3 (InstructionError::InvalidAccountData) + { + use light_test_utils::mint_2022::{ + create_mint_22_with_extension_types, create_token_22_account, + }; + use spl_token_2022::extension::ExtensionType; + + println!("Test 11: Token account passed as mint"); + + context.token_account_keypair = Keypair::new(); + + // Create a real T22 mint + let (mint_keypair, _config) = create_mint_22_with_extension_types( + &mut context.rpc, + &context.payer, + 9, + &[ExtensionType::TransferFeeConfig], + ) + .await; + let real_mint = mint_keypair.pubkey(); + + // Create a T22 token account for that mint + let t22_token_account = + create_token_22_account(&mut context.rpc, &context.payer, &real_mint, &payer_pubkey) + .await; + + // Try to create CToken with the token account as mint (should fail) + let compressible_params = CompressibleParams { + compressible_config: context.compressible_config, + rent_sponsor: context.rent_sponsor, + pre_pay_num_epochs: 2, + lamports_per_write: Some(100), + compress_to_account_pubkey: None, + token_account_version: light_ctoken_interface::state::TokenDataVersion::ShaFlat, + compression_only: true, // Required for restricted extensions + }; + + let create_token_account_ix = CreateCTokenAccount::new( + payer_pubkey, + context.token_account_keypair.pubkey(), + t22_token_account, // Token account, not mint! + context.owner_keypair.pubkey(), + ) + .with_compressible(compressible_params) + .instruction() + .unwrap(); + + let result = context + .rpc + .create_and_send_transaction( + &[create_token_account_ix], + &payer_pubkey, + &[&context.payer, &context.token_account_keypair], + ) + .await; + + // Should fail with InstructionError::InvalidAccountData (3) because is_valid_mint returns false + // for token accounts (AccountType=2 at offset 165) + light_program_test::utils::assert::assert_rpc_error(result, 0, 3).unwrap(); + } + + // Test 12: Invalid token_account_version + // Only version 3 (ShaFlat) is supported. V1 and V2 are rejected. + // Error: 2 (InstructionError::InvalidInstructionData) + { + println!("Test 12: Invalid token_account_version"); + + context.token_account_keypair = Keypair::new(); + + // Build instruction using SDK with V1 version (not supported for create) + let compressible_params = CompressibleParams { + compressible_config: context.compressible_config, + rent_sponsor: context.rent_sponsor, + pre_pay_num_epochs: 2, + lamports_per_write: Some(100), + compress_to_account_pubkey: None, + token_account_version: light_ctoken_interface::state::TokenDataVersion::V1, // Not supported! + compression_only: false, + }; + + let create_ix = CreateCTokenAccount::new( + payer_pubkey, + context.token_account_keypair.pubkey(), + context.mint_pubkey, + context.owner_keypair.pubkey(), + ) + .with_compressible(compressible_params) + .instruction() + .unwrap(); + + let result = context + .rpc + .create_and_send_transaction( + &[create_ix], + &payer_pubkey, + &[&context.payer, &context.token_account_keypair], + ) + .await; + + // Should fail with InstructionError::InvalidInstructionData (2) + // Only version 3 (ShaFlat) is supported for compressible accounts + light_program_test::utils::assert::assert_rpc_error(result, 0, 2).unwrap(); + } } diff --git a/program-tests/compressed-token-test/tests/ctoken/create_ata.rs b/program-tests/compressed-token-test/tests/ctoken/create_ata.rs index be001e0c6d..256d4eafe8 100644 --- a/program-tests/compressed-token-test/tests/ctoken/create_ata.rs +++ b/program-tests/compressed-token-test/tests/ctoken/create_ata.rs @@ -123,6 +123,78 @@ async fn test_create_compressible_ata() { ) .await; } + + // Test 6: Maximum prepaid epochs (255) - boundary test + { + // Use different mint for sixth ATA + context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + + let compressible_data = CompressibleData { + compression_authority: context.compression_authority, + rent_sponsor: payer_pubkey, // Use payer as rent sponsor for large epoch payment + num_prepaid_epochs: 255, // Maximum u8 value + lamports_per_write: Some(100), + account_version: light_ctoken_interface::state::TokenDataVersion::ShaFlat, + compress_to_pubkey: false, + payer: payer_pubkey, + }; + + create_and_assert_ata( + &mut context, + Some(compressible_data), + false, // Non-idempotent + "max_prepaid_epochs_255", + ) + .await; + } + + // Test 7: Owner equals mint pubkey - edge case + // This is an unusual but valid configuration where the owner of the ATA + // is the same pubkey as the mint. Should succeed. + { + // Use a new unique pubkey that will serve as both owner and mint + let owner_and_mint = solana_sdk::pubkey::Pubkey::new_unique(); + context.mint_pubkey = owner_and_mint; + + // Temporarily change the owner keypair to use the same pubkey as mint + // Note: This is a degenerate case but should still work + let compressible_params = CompressibleParams { + compressible_config: context.compressible_config, + rent_sponsor: context.rent_sponsor, + pre_pay_num_epochs: 2, + lamports_per_write: Some(100), + compress_to_account_pubkey: None, + token_account_version: light_ctoken_interface::state::TokenDataVersion::ShaFlat, + compression_only: true, + }; + + let create_ata_ix = CreateAssociatedCTokenAccount::new( + payer_pubkey, + owner_and_mint, // Owner == Mint + owner_and_mint, // Mint == Owner + ) + .with_compressible(compressible_params) + .instruction() + .unwrap(); + + context + .rpc + .create_and_send_transaction(&[create_ata_ix], &payer_pubkey, &[&context.payer]) + .await + .unwrap(); + + // Verify ATA was created at the expected address + let (expected_ata, _) = derive_ctoken_ata(&owner_and_mint, &owner_and_mint); + let account = context.rpc.get_account(expected_ata).await.unwrap(); + assert!( + account.is_some(), + "ATA with owner==mint should be created successfully" + ); + println!( + "Successfully created ATA with owner==mint at {}", + expected_ata + ); + } } #[tokio::test] @@ -699,6 +771,131 @@ async fn test_create_ata_failing() { // Solana runtime rejects this as unauthorized signer privilege escalation. light_program_test::utils::assert::assert_rpc_error(result, 0, 19).unwrap(); } + + // Test 11: Non-compressible ATA for mint with restricted extensions + // Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) + // require the compression_only marker which is part of the Compressible extension. + // Error: 6115 (MissingCompressibleConfig) + { + use anchor_lang::prelude::borsh::BorshSerialize; + use light_ctoken_interface::instructions::create_associated_token_account::CreateAssociatedTokenAccountInstructionData; + use light_test_utils::mint_2022::create_mint_22_with_extension_types; + use solana_sdk::instruction::Instruction; + use spl_token_2022::extension::ExtensionType; + + println!("Test 11: Non-compressible ATA for mint with restricted extensions"); + + // Create a Token-2022 mint with TransferFeeConfig (a restricted extension) + let (mint_keypair, _config) = create_mint_22_with_extension_types( + &mut context.rpc, + &context.payer, + 9, // decimals + &[ExtensionType::TransferFeeConfig], + ) + .await; + let mint_with_restricted_ext = mint_keypair.pubkey(); + + // Use a new owner for this test + let owner = solana_sdk::pubkey::Pubkey::new_unique(); + + // Derive ATA address + let (ata_pubkey, bump) = derive_ctoken_ata(&owner, &mint_with_restricted_ext); + + // Build instruction data with compressible_config: None (non-compressible) + let instruction_data = CreateAssociatedTokenAccountInstructionData { + bump, + compressible_config: None, // Non-compressible! + }; + + let mut data = vec![100]; // CreateAssociatedCTokenAccount discriminator + instruction_data.serialize(&mut data).unwrap(); + + // Account order: owner, mint, payer, ata, system_program + let ix = Instruction { + program_id: light_compressed_token::ID, + accounts: vec![ + solana_sdk::instruction::AccountMeta::new_readonly(owner, false), + solana_sdk::instruction::AccountMeta::new_readonly(mint_with_restricted_ext, false), + solana_sdk::instruction::AccountMeta::new(payer_pubkey, true), + solana_sdk::instruction::AccountMeta::new(ata_pubkey, false), + solana_sdk::instruction::AccountMeta::new_readonly( + solana_sdk::pubkey::Pubkey::default(), + false, + ), + ], + data, + }; + + let result = context + .rpc + .create_and_send_transaction(&[ix], &payer_pubkey, &[&context.payer]) + .await; + + // Should fail with MissingCompressibleConfig (6115) + // Rationale: Mints with restricted extensions must be marked as compression_only, + // and that marker is part of the Compressible extension + light_program_test::utils::assert::assert_rpc_error(result, 0, 6115).unwrap(); + } + + // Test 12: Token account passed as mint + // Passing a valid T22 token account instead of a mint should fail validation. + // The is_valid_mint function checks AccountType at offset 165 - token accounts have AccountType=2. + // Error: 3 (InstructionError::InvalidAccountData) + { + use light_test_utils::mint_2022::{ + create_mint_22_with_extension_types, create_token_22_account, + }; + use spl_token_2022::extension::ExtensionType; + + println!("Test 12: Token account passed as mint (ATA)"); + + // Create a real T22 mint + let (mint_keypair, _config) = create_mint_22_with_extension_types( + &mut context.rpc, + &context.payer, + 9, + &[ExtensionType::TransferFeeConfig], + ) + .await; + let real_mint = mint_keypair.pubkey(); + + // Create a T22 token account for that mint + let t22_token_account = + create_token_22_account(&mut context.rpc, &context.payer, &real_mint, &payer_pubkey) + .await; + + // Use a new owner for this test + let owner = solana_sdk::pubkey::Pubkey::new_unique(); + + // Try to create ATA with the token account as mint (should fail) + let compressible_params = CompressibleParams { + compressible_config: context.compressible_config, + rent_sponsor: context.rent_sponsor, + pre_pay_num_epochs: 2, + lamports_per_write: Some(100), + compress_to_account_pubkey: None, + token_account_version: light_ctoken_interface::state::TokenDataVersion::ShaFlat, + compression_only: true, // Required for restricted extensions + }; + + let create_ata_ix = CreateAssociatedCTokenAccount::new( + payer_pubkey, + owner, + t22_token_account, // Token account, not mint! + ) + .with_compressible(compressible_params) + .instruction() + .unwrap(); + + let result = context + .rpc + .create_and_send_transaction(&[create_ata_ix], &payer_pubkey, &[&context.payer]) + .await; + + // Should fail with InstructionError::InvalidAccountData (3) because is_valid_mint returns false + // for token accounts (AccountType=2 at offset 165) + light_program_test::utils::assert::assert_rpc_error(result, 0, 3).unwrap(); + } } #[tokio::test] diff --git a/programs/compressed-token/anchor/src/lib.rs b/programs/compressed-token/anchor/src/lib.rs index 0081f30cdf..80dd79fca5 100644 --- a/programs/compressed-token/anchor/src/lib.rs +++ b/programs/compressed-token/anchor/src/lib.rs @@ -249,305 +249,305 @@ pub mod light_compressed_token { #[error_code] pub enum ErrorCode { #[msg("public keys and amounts must be of same length")] - PublicKeyAmountMissmatch, + PublicKeyAmountMissmatch, // 6000 #[msg("ComputeInputSumFailed")] - ComputeInputSumFailed, + ComputeInputSumFailed, // 6001 #[msg("ComputeOutputSumFailed")] - ComputeOutputSumFailed, + ComputeOutputSumFailed, // 6002 #[msg("ComputeCompressSumFailed")] - ComputeCompressSumFailed, + ComputeCompressSumFailed, // 6003 #[msg("ComputeDecompressSumFailed")] - ComputeDecompressSumFailed, + ComputeDecompressSumFailed, // 6004 #[msg("SumCheckFailed")] - SumCheckFailed, + SumCheckFailed, // 6005 #[msg("DecompressRecipientUndefinedForDecompress")] - DecompressRecipientUndefinedForDecompress, + DecompressRecipientUndefinedForDecompress, // 6006 #[msg("CompressedPdaUndefinedForDecompress")] - CompressedPdaUndefinedForDecompress, + CompressedPdaUndefinedForDecompress, // 6007 #[msg("DeCompressAmountUndefinedForDecompress")] - DeCompressAmountUndefinedForDecompress, + DeCompressAmountUndefinedForDecompress, // 6008 #[msg("CompressedPdaUndefinedForCompress")] - CompressedPdaUndefinedForCompress, + CompressedPdaUndefinedForCompress, // 6009 #[msg("DeCompressAmountUndefinedForCompress")] - DeCompressAmountUndefinedForCompress, + DeCompressAmountUndefinedForCompress, // 6010 #[msg("DelegateSignerCheckFailed")] - DelegateSignerCheckFailed, + DelegateSignerCheckFailed, // 6011 #[msg("Minted amount greater than u64::MAX")] - MintTooLarge, + MintTooLarge, // 6012 #[msg("SplTokenSupplyMismatch")] - SplTokenSupplyMismatch, + SplTokenSupplyMismatch, // 6013 #[msg("HeapMemoryCheckFailed")] - HeapMemoryCheckFailed, + HeapMemoryCheckFailed, // 6014 #[msg("The instruction is not callable")] - InstructionNotCallable, + InstructionNotCallable, // 6015 #[msg("ArithmeticUnderflow")] - ArithmeticUnderflow, + ArithmeticUnderflow, // 6016 #[msg("HashToFieldError")] - HashToFieldError, + HashToFieldError, // 6017 #[msg("Expected the authority to be also a mint authority")] - InvalidAuthorityMint, + InvalidAuthorityMint, // 6018 #[msg("Provided authority is not the freeze authority")] - InvalidFreezeAuthority, - InvalidDelegateIndex, - TokenPoolPdaUndefined, + InvalidFreezeAuthority, // 6019 + InvalidDelegateIndex, // 6020 + TokenPoolPdaUndefined, // 6021 #[msg("Compress or decompress recipient is the same account as the token pool pda.")] - IsTokenPoolPda, - InvalidTokenPoolPda, - NoInputTokenAccountsProvided, - NoInputsProvided, - MintHasNoFreezeAuthority, - MintWithInvalidExtension, + IsTokenPoolPda, // 6022 + InvalidTokenPoolPda, // 6023 + NoInputTokenAccountsProvided, // 6024 + NoInputsProvided, // 6025 + MintHasNoFreezeAuthority, // 6026 + MintWithInvalidExtension, // 6027 #[msg("The token account balance is less than the remaining amount.")] - InsufficientTokenAccountBalance, + InsufficientTokenAccountBalance, // 6028 #[msg("Max number of token pools reached.")] - InvalidTokenPoolBump, - FailedToDecompress, - FailedToBurnSplTokensFromTokenPool, - NoMatchingBumpFound, - NoAmount, - AmountsAndAmountProvided, + InvalidTokenPoolBump, // 6029 + FailedToDecompress, // 6030 + FailedToBurnSplTokensFromTokenPool, // 6031 + NoMatchingBumpFound, // 6032 + NoAmount, // 6033 + AmountsAndAmountProvided, // 6034 #[msg("Cpi context set and set first is not usable with burn, compression(transfer ix) or decompress(transfer).")] - CpiContextSetNotUsable, - MintIsNone, - InvalidMintPda, + CpiContextSetNotUsable, // 6035 + MintIsNone, // 6036 + InvalidMintPda, // 6037 #[msg("Sum inputs mint indices not in ascending order.")] - InputsOutOfOrder, + InputsOutOfOrder, // 6038 #[msg("Sum check, too many mints (max 5).")] - TooManyMints, - InvalidExtensionType, - InstructionDataExpectedDelegate, - ZeroCopyExpectedDelegate, + TooManyMints, // 6039 + InvalidExtensionType, // 6040 + InstructionDataExpectedDelegate, // 6041 + ZeroCopyExpectedDelegate, // 6042 #[msg("Unsupported TLV extension type - only CompressedOnly is currently implemented")] - UnsupportedTlvExtensionType, + UnsupportedTlvExtensionType, // 6043 // Mint Action specific errors #[msg("Mint action requires at least one action")] - MintActionNoActionsProvided, + MintActionNoActionsProvided, // 6044 #[msg("Missing mint signer account for SPL mint creation")] - MintActionMissingSplMintSigner, + MintActionMissingSplMintSigner, // 6045 #[msg("Missing system account configuration for mint action")] - MintActionMissingSystemAccount, + MintActionMissingSystemAccount, // 6046 #[msg("Invalid mint bump seed provided")] - MintActionInvalidMintBump, + MintActionInvalidMintBump, // 6047 #[msg("Missing mint account for decompressed mint operations")] - MintActionMissingMintAccount, + MintActionMissingMintAccount, // 6048 #[msg("Missing token pool account for decompressed mint operations")] - MintActionMissingTokenPoolAccount, + MintActionMissingTokenPoolAccount, // 6049 #[msg("Missing token program for SPL operations")] - MintActionMissingTokenProgram, + MintActionMissingTokenProgram, // 6050 #[msg("Mint account does not match expected mint")] - MintAccountMismatch, + MintAccountMismatch, // 6051 #[msg("Invalid or missing authority for compression operation")] - InvalidCompressAuthority, + InvalidCompressAuthority, // 6052 #[msg("Invalid queue index configuration")] - MintActionInvalidQueueIndex, + MintActionInvalidQueueIndex, // 6053 #[msg("Mint output serialization failed")] - MintActionSerializationFailed, + MintActionSerializationFailed, // 6054 #[msg("Proof required for mint action but not provided")] - MintActionProofMissing, + MintActionProofMissing, // 6055 #[msg("Unsupported mint action type")] - MintActionUnsupportedActionType, + MintActionUnsupportedActionType, // 6056 #[msg("Metadata operations require decompressed mints")] - MintActionMetadataNotDecompressed, + MintActionMetadataNotDecompressed, // 6057 #[msg("Missing metadata extension in mint")] - MintActionMissingMetadataExtension, + MintActionMissingMetadataExtension, // 6058 #[msg("Extension index out of bounds")] - MintActionInvalidExtensionIndex, + MintActionInvalidExtensionIndex, // 6059 #[msg("Invalid metadata value encoding")] - MintActionInvalidMetadataValue, + MintActionInvalidMetadataValue, // 6060 #[msg("Invalid metadata key encoding")] - MintActionInvalidMetadataKey, + MintActionInvalidMetadataKey, // 6061 #[msg("Extension at index is not a TokenMetadata extension")] - MintActionInvalidExtensionType, + MintActionInvalidExtensionType, // 6062 #[msg("Metadata key not found")] - MintActionMetadataKeyNotFound, + MintActionMetadataKeyNotFound, // 6063 #[msg("Missing executing system accounts for mint action")] - MintActionMissingExecutingAccounts, + MintActionMissingExecutingAccounts, // 6064 #[msg("Invalid mint authority for mint action")] - MintActionInvalidMintAuthority, + MintActionInvalidMintAuthority, // 6065 #[msg("Invalid mint PDA derivation in mint action")] - MintActionInvalidMintPda, + MintActionInvalidMintPda, // 6066 #[msg("Missing system accounts for queue index calculation")] - MintActionMissingSystemAccountsForQueue, + MintActionMissingSystemAccountsForQueue, // 6067 #[msg("Account data serialization failed in mint output")] - MintActionOutputSerializationFailed, + MintActionOutputSerializationFailed, // 6068 #[msg("Mint amount too large, would cause overflow")] - MintActionAmountTooLarge, + MintActionAmountTooLarge, // 6069 #[msg("Initial supply must be 0 for new mint creation")] - MintActionInvalidInitialSupply, + MintActionInvalidInitialSupply, // 6070 #[msg("Mint version not supported")] - MintActionUnsupportedVersion, + MintActionUnsupportedVersion, // 6071 #[msg("New mint must start as compressed")] - MintActionInvalidCompressionState, - MintActionUnsupportedOperation, + MintActionInvalidCompressionState, // 6072 + MintActionUnsupportedOperation, // 6073 // Close account specific errors #[msg("Cannot close account with non-zero token balance")] - NonNativeHasBalance, + NonNativeHasBalance, // 6074 #[msg("Authority signature does not match expected owner")] - OwnerMismatch, + OwnerMismatch, // 6075 #[msg("Account is frozen and cannot perform this operation")] - AccountFrozen, + AccountFrozen, // 6076 // Account creation specific errors #[msg("Account size insufficient for token account")] - InsufficientAccountSize, + InsufficientAccountSize, // 6077 #[msg("Account already initialized")] - AlreadyInitialized, + AlreadyInitialized, // 6078 #[msg("Extension instruction data invalid")] - InvalidExtensionInstructionData, + InvalidExtensionInstructionData, // 6079 #[msg("Lamports amount too large")] - MintActionLamportsAmountTooLarge, + MintActionLamportsAmountTooLarge, // 6080 #[msg("Invalid token program provided")] - InvalidTokenProgram, + InvalidTokenProgram, // 6081 // Transfer2 specific errors #[msg("Cannot access system accounts for CPI context write operations")] - Transfer2CpiContextWriteInvalidAccess, + Transfer2CpiContextWriteInvalidAccess, // 6082 #[msg("SOL pool operations not supported with CPI context write")] - Transfer2CpiContextWriteWithSolPool, + Transfer2CpiContextWriteWithSolPool, // 6083 #[msg("Change account must not contain token data")] - Transfer2InvalidChangeAccountData, + Transfer2InvalidChangeAccountData, // 6084 #[msg("Cpi context expected but not provided.")] - CpiContextExpected, + CpiContextExpected, // 6085 #[msg("CPI accounts slice exceeds provided account infos")] - CpiAccountsSliceOutOfBounds, + CpiAccountsSliceOutOfBounds, // 6086 // CompressAndClose specific errors #[msg("CompressAndClose requires a destination account for rent lamports")] - CompressAndCloseDestinationMissing, + CompressAndCloseDestinationMissing, // 6087 #[msg("CompressAndClose requires an authority account")] - CompressAndCloseAuthorityMissing, + CompressAndCloseAuthorityMissing, // 6088 #[msg("CompressAndClose: Compressed token owner does not match expected owner")] - CompressAndCloseInvalidOwner, + CompressAndCloseInvalidOwner, // 6089 #[msg("CompressAndClose: Compression amount must match the full token balance")] - CompressAndCloseAmountMismatch, + CompressAndCloseAmountMismatch, // 6090 #[msg("CompressAndClose: Token account balance must match compressed output amount")] - CompressAndCloseBalanceMismatch, + CompressAndCloseBalanceMismatch, // 6091 #[msg("CompressAndClose: Compressed token must not have a delegate")] - CompressAndCloseDelegateNotAllowed, + CompressAndCloseDelegateNotAllowed, // 6092 #[msg("CompressAndClose: Invalid compressed token version")] - CompressAndCloseInvalidVersion, + CompressAndCloseInvalidVersion, // 6093 #[msg("InvalidAddressTree")] - InvalidAddressTree, + InvalidAddressTree, // 6094 #[msg("Too many compression transfers. Maximum 40 transfers allowed per instruction")] - TooManyCompressionTransfers, + TooManyCompressionTransfers, // 6095 #[msg("Missing fee payer for compressions-only operation")] - CompressionsOnlyMissingFeePayer, + CompressionsOnlyMissingFeePayer, // 6096 #[msg("Missing CPI authority PDA for compressions-only operation")] - CompressionsOnlyMissingCpiAuthority, + CompressionsOnlyMissingCpiAuthority, // 6097 #[msg("Cpi authority pda expected but not provided.")] - ExpectedCpiAuthority, + ExpectedCpiAuthority, // 6098 #[msg("InvalidRentSponsor")] - InvalidRentSponsor, - TooManyMintToRecipients, + InvalidRentSponsor, // 6099 + TooManyMintToRecipients, // 6100 #[msg("Prefunding for exactly 1 epoch is not allowed due to epoch boundary timing risk. Use 0 or 2+ epochs.")] - OneEpochPrefundingNotAllowed, + OneEpochPrefundingNotAllowed, // 6101 #[msg("Duplicate mint index detected in inputs, outputs, or compressions")] - DuplicateMint, + DuplicateMint, // 6102 #[msg("Invalid compressed mint address derivation")] - MintActionInvalidCompressedMintAddress, + MintActionInvalidCompressedMintAddress, // 6103 #[msg("Invalid CPI context for create mint operation")] - MintActionInvalidCpiContextForCreateMint, + MintActionInvalidCpiContextForCreateMint, // 6104 #[msg("Invalid address tree pubkey in CPI context")] - MintActionInvalidCpiContextAddressTreePubkey, + MintActionInvalidCpiContextAddressTreePubkey, // 6105 #[msg("CompressAndClose: Cannot use the same compressed output account for multiple closures")] - CompressAndCloseDuplicateOutput, + CompressAndCloseDuplicateOutput, // 6106 #[msg( "CompressAndClose by compression authority requires compressed token account in outputs" )] - CompressAndCloseOutputMissing, + CompressAndCloseOutputMissing, // 6107 // CMint (decompressed compressed mint) specific errors #[msg("Missing mint signer account for mint action")] - MintActionMissingMintSigner, + MintActionMissingMintSigner, // 6108 #[msg("Missing CMint account for decompress mint action")] - MintActionMissingCMintAccount, + MintActionMissingCMintAccount, // 6109 #[msg("CMint account already exists")] - CMintAlreadyExists, + CMintAlreadyExists, // 6110 #[msg("Invalid CMint account owner")] - InvalidCMintOwner, + InvalidCMintOwner, // 6111 #[msg("Failed to deserialize CMint account data")] - CMintDeserializationFailed, + CMintDeserializationFailed, // 6112 #[msg("Failed to resize CMint account")] - CMintResizeFailed, + CMintResizeFailed, // 6113 // CMint Compressibility errors #[msg("Invalid rent payment - must be >= 2 (CMint is always compressible)")] - InvalidRentPayment, + InvalidRentPayment, // 6114 #[msg("Missing compressible config account for CMint")] - MissingCompressibleConfig, + MissingCompressibleConfig, // 6115 #[msg("Missing rent sponsor account for CMint")] - MissingRentSponsor, + MissingRentSponsor, // 6116 #[msg("Rent payment exceeds max funded epochs")] - RentPaymentExceedsMax, + RentPaymentExceedsMax, // 6117 #[msg("Write top-up exceeds maximum allowed")] - WriteTopUpExceedsMaximum, + WriteTopUpExceedsMaximum, // 6118 #[msg("Failed to calculate CMint top-up amount")] - CMintTopUpCalculationFailed, + CMintTopUpCalculationFailed, // 6119 // CompressAndCloseCMint specific errors #[msg("CMint is not decompressed")] - CMintNotDecompressed, + CMintNotDecompressed, // 6120 #[msg("CMint is missing Compressible extension")] - CMintMissingCompressibleExtension, + CMintMissingCompressibleExtension, // 6121 #[msg("CMint is not compressible (rent not expired)")] - CMintNotCompressible, + CMintNotCompressible, // 6122 #[msg("Cannot combine DecompressMint and CompressAndCloseCMint in same instruction")] - CannotDecompressAndCloseInSameInstruction, + CannotDecompressAndCloseInSameInstruction, // 6123 #[msg("CMint account does not match compressed_mint.metadata.mint")] - InvalidCMintAccount, + InvalidCMintAccount, // 6124 #[msg("Mint data required in instruction when not decompressed")] - MintDataRequired, + MintDataRequired, // 6125 // Extension validation errors #[msg("Invalid mint account data")] - InvalidMint, + InvalidMint, // 6126 #[msg("Token operations blocked - mint is paused")] - MintPaused, + MintPaused, // 6127 #[msg("Mint account required for transfer when account has PausableAccount extension")] - MintRequiredForTransfer, + MintRequiredForTransfer, // 6128 #[msg("Non-zero transfer fees are not supported")] - NonZeroTransferFeeNotSupported, + NonZeroTransferFeeNotSupported, // 6129 #[msg("Transfer hooks with non-nil program_id are not supported")] - TransferHookNotSupported, + TransferHookNotSupported, // 6130 #[msg("Mint has extensions that require compression_only mode")] - CompressionOnlyRequired, + CompressionOnlyRequired, // 6131 #[msg("CompressAndClose: Compressed token mint does not match source token account mint")] - CompressAndCloseInvalidMint, + CompressAndCloseInvalidMint, // 6132 #[msg("CompressAndClose: Missing required CompressedOnly extension in output TLV")] - CompressAndCloseMissingCompressedOnlyExtension, + CompressAndCloseMissingCompressedOnlyExtension, // 6133 #[msg("CompressAndClose: CompressedOnly mint_account_index must be 0")] - CompressAndCloseInvalidMintAccountIndex, + CompressAndCloseInvalidMintAccountIndex, // 6134 #[msg( "CompressAndClose: Delegated amount mismatch between ctoken and CompressedOnly extension" )] - CompressAndCloseDelegatedAmountMismatch, + CompressAndCloseDelegatedAmountMismatch, // 6135 #[msg("CompressAndClose: Delegate mismatch between ctoken and compressed token output")] - CompressAndCloseInvalidDelegate, + CompressAndCloseInvalidDelegate, // 6136 #[msg("CompressAndClose: Withheld transfer fee mismatch")] - CompressAndCloseWithheldFeeMismatch, + CompressAndCloseWithheldFeeMismatch, // 6137 #[msg("CompressAndClose: Frozen state mismatch")] - CompressAndCloseFrozenMismatch, + CompressAndCloseFrozenMismatch, // 6138 #[msg("TLV extensions require version 3 (ShaFlat)")] - TlvRequiresVersion3, + TlvRequiresVersion3, // 6139 #[msg("CToken account has extensions that cannot be compressed. Only Compressible extension or no extensions allowed.")] - CTokenHasDisallowedExtensions, + CTokenHasDisallowedExtensions, // 6140 #[msg("CompressAndClose: rent_sponsor_is_signer flag does not match actual signer")] - RentSponsorIsSignerMismatch, + RentSponsorIsSignerMismatch, // 6141 #[msg("Mint has restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook, DefaultAccountState) must not create compressed token accounts.")] - MintHasRestrictedExtensions, + MintHasRestrictedExtensions, // 6142 #[msg("Decompress: CToken delegate does not match input compressed account delegate")] - DecompressDelegateMismatch, + DecompressDelegateMismatch, // 6143 #[msg("Mint cache capacity exceeded (max 5 unique mints)")] - MintCacheCapacityExceeded, + MintCacheCapacityExceeded, // 6144 #[msg("in_lamports field is not yet implemented")] - InLamportsUnimplemented, + InLamportsUnimplemented, // 6145 #[msg("out_lamports field is not yet implemented")] - OutLamportsUnimplemented, + OutLamportsUnimplemented, // 6146 #[msg("Mints with restricted extensions require compressible accounts")] - CompressibleRequired, + CompressibleRequired, // 6147 #[msg("CMint account not found")] - CMintNotFound, + CMintNotFound, // 6148 #[msg("CompressedOnly inputs must decompress to CToken account, not SPL token account")] - CompressedOnlyRequiresCTokenDecompress, + CompressedOnlyRequiresCTokenDecompress, // 6149 #[msg("Invalid token data version")] - InvalidTokenDataVersion, + InvalidTokenDataVersion, // 6150 #[msg("compression_only can only be set for mints with restricted extensions")] - CompressionOnlyNotAllowed, + CompressionOnlyNotAllowed, // 6151 #[msg("Associated token accounts must have compression_only set")] - AtaRequiresCompressionOnly, + AtaRequiresCompressionOnly, // 6152 } /// Anchor error code offset - error codes start at 6000 diff --git a/programs/compressed-token/program/docs/ctoken/CREATE.md b/programs/compressed-token/program/docs/ctoken/CREATE.md index 1f68f51fa2..4e6cccf017 100644 --- a/programs/compressed-token/program/docs/ctoken/CREATE.md +++ b/programs/compressed-token/program/docs/ctoken/CREATE.md @@ -1,15 +1,6 @@ # Instructions - -**Instruction Schema:** -1. every instruction description must include the sections: - - **path** path to instruction code in the program - - **description** highlevel description what the instruction does including accounts used and their state layout (paths to the code), usage flows what the instruction does - - **instruction_data** paths to code where instruction data structs are defined - - **Accounts** accounts in order including checks - - **Instruction logic and checks** - - **Errors** possible errors and description what causes these errors - +- This file documents create ctoken account and create associated ctoken account. ## 1. create ctoken account @@ -38,7 +29,7 @@ - `compressible_config`: Optional `CompressibleExtensionInstructionData` (None = non-compressible account) 2. Instruction data with compressible extension program-libs/ctoken-interface/src/instructions/extensions/compressible.rs - - `token_account_version`: Version of the compressed token account hashing scheme (u8) + - `token_account_version`: Version of the compressed token account hashing scheme (u8). Must be 3 (ShaFlat) - only version 3 is supported. - `rent_payment`: Number of epochs to prepay for rent (u8) - `rent_payment = 1` is explicitly forbidden to prevent epoch boundary timing edge case (its rent for the current rent epoch) - Allowed values: 0 (no prefunding) or 2+ epochs (safe buffer) @@ -95,6 +86,9 @@ 4.3. Validate compression_only requirement for restricted extensions: - If mint has restricted extensions (e.g., TransferFee) and compression_only == 0 - Error: `ErrorCode::CompressionOnlyRequired` + 4.3.1. Validate compression_only is only set for mints with restricted extensions: + - If compression_only != 0 and mint has no restricted extensions + - Error: `ErrorCode::CompressionOnlyNotAllowed` 4.4. Calculate account size based on mint extensions (includes Compressible extension) 4.5. Calculate rent (rent exemption + prepaid epochs rent + compression incentive) 4.6. Check whether rent_payer is custom fee payer (rent_payer != config.rent_sponsor) @@ -109,6 +103,21 @@ - If custom fee payer, set custom fee payer as ctoken account rent_sponsor - Else set config.rent_sponsor as ctoken account rent_sponsor - Set `last_claimed_slot` to current slot (tracks when rent was last claimed/initialized) + - Validate token_account_version == 3 (ShaFlat) + - Error: `ProgramError::InvalidInstructionData` if version != 3 + - Validate write_top_up <= config.rent_config.max_top_up + - Error: `CTokenError::WriteTopUpExceedsMaximum` if exceeded + - Validate mint account (if initialized): + - Check mint owner is SPL Token, Token-2022, or CToken program + - Error: `ProgramError::IncorrectProgramId` if invalid owner + - Check mint structure is valid (82 bytes for SPL, or has AccountType marker for T22) + - Error: `ProgramError::InvalidAccountData` if invalid structure + - Cache decimals from mint account in extension + 5. If without compressible account (non-compressible path): + 5.1. Validate mint does not have restricted extensions + - Check: `!mint_extensions.has_restricted_extensions()` + - Error: `ErrorCode::MissingCompressibleConfig` if mint has restricted extensions + - Rationale: Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) must be marked as compression_only, and that marker is part of the Compressible extension **Errors:** - `ProgramError::BorshIoError` (error code: 15) - Failed to deserialize CreateTokenAccountInstructionData from instruction_data bytes @@ -116,15 +125,19 @@ - `AccountError::InvalidSigner` (error code: 12015) - token_account or payer is not a signer when required - `AccountError::AccountNotMutable` (error code: 12008) - token_account or payer is not mutable when required - `AccountError::AccountOwnedByWrongProgram` (error code: 12007) - Config account not owned by LightRegistry program - - `ProgramError::InvalidAccountData` (error code: 4) - CompressibleConfig pod deserialization fails or compress_to_pubkey.check_seeds() fails - - `ProgramError::InvalidInstructionData` (error code: 3) - compressible_config is None in instruction data when compressible accounts provided, or extension data invalid + - `ProgramError::InvalidAccountData` (error code: 4) - CompressibleConfig pod deserialization fails, compress_to_pubkey.check_seeds() fails, or invalid mint structure + - `ProgramError::InvalidInstructionData` (error code: 3) - compressible_config is None in instruction data when compressible accounts provided, extension data invalid, or token_account_version != 3 - `ProgramError::MissingRequiredSignature` (error code: 8) - Custom rent_payer is not a signer - `ProgramError::UnsupportedSysvar` (error code: 17) - Failed to get Clock sysvar + - `ProgramError::IncorrectProgramId` (error code: 1) - Mint account owner is not SPL Token, Token-2022, or CToken program - `CompressibleError::InvalidState` (error code: 19002) - CompressibleConfig is not in active state - `ErrorCode::InsufficientAccountSize` (error code: 6077) - token_account data length < 165 bytes (non-compressible) or < COMPRESSIBLE_TOKEN_ACCOUNT_SIZE (compressible) - - `ErrorCode::InvalidCompressAuthority` (error code: 6052) - compressible_config is Some but compressible_config_account is None during extension initialization - - `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6116) - rent_payment is exactly 1 epoch, which is forbidden due to epoch boundary timing edge case + - `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6101) - rent_payment is exactly 1 epoch, which is forbidden due to epoch boundary timing edge case - `ErrorCode::CompressionOnlyRequired` (error code: 6131) - Mint has restricted extensions (e.g., TransferFee) but compression_only is not set in instruction data + - `ErrorCode::MissingCompressibleConfig` (error code: 6115) - Either: (1) compressible_config is Some in instruction data but compressible accounts are missing, or (2) non-compressible account creation attempted for mint with restricted extensions + - `ErrorCode::CompressionOnlyNotAllowed` (error code: 6151) - compression_only is set but mint has no restricted extensions + - `CTokenError::WriteTopUpExceedsMaximum` (error code: 18042) - write_top_up exceeds config.rent_config.max_top_up + - `CTokenError::MissingCompressibleExtension` (error code: 18056) - Compressible extension initialization failed internally ## 2. create associated ctoken account @@ -140,11 +153,14 @@ 4. Associated token accounts cannot use compress_to_pubkey (always compress to owner) 5. Owner and mint are provided as accounts, bump is provided via instruction data 6. Token account must be uninitialized (owned by system program) unless idempotent mode + 7. ATAs for mints with restricted extensions must be compressible (the compression_only marker is part of the Compressible extension) **Instruction data:** 1. instruction data is defined in path: program-libs/ctoken-interface/src/instructions/create_associated_token_account.rs - `bump`: PDA bump seed for derivation (u8) - - `compressible_config`: Optional `CompressibleExtensionInstructionData`, same as create ctoken account but compress_to_account_pubkey must be None + - `compressible_config`: Optional `CompressibleExtensionInstructionData`, same as create ctoken account but: + - `compress_to_account_pubkey` must be None (ATAs always compress to owner) + - `compression_only` must be non-zero (compressible ATAs require compression_only) **Accounts:** 1. owner @@ -181,11 +197,14 @@ 4. Verify account is system-owned (uninitialized) - Error: `ProgramError::IllegalOwner` if not owned by system program 5. If compressible: - - Validate rent_payment is not exactly 1 epoch (same as create ctoken account step 3.0) + - Validate rent_payment is not exactly 1 epoch (same as create ctoken account step 4.1) - Check: `compressible_config.rent_payment != 1` - Error: `ErrorCode::OneEpochPrefundingNotAllowed` if validation fails - Reject if compress_to_account_pubkey is Some (not allowed for ATAs) - Error: `ProgramError::InvalidInstructionData` if compress_to_account_pubkey is Some + - Validate compression_only is set (required for compressible ATAs) + - Check: `compressible_config.compression_only != 0` + - Error: `ErrorCode::AtaRequiresCompressionOnly` if compression_only == 0 - Parse additional accounts: config, rent_payer - Validate CompressibleConfig is active (not inactive or deprecated) - Calculate account size based on mint extensions (includes Compressible extension) @@ -198,7 +217,11 @@ - Create ATA PDA with rent_sponsor PDA paying rent exemption - Transfer compression incentive from fee_payer to account via CPI 6. If not compressible: - - Create ATA PDA with fee_payer paying rent exemption (base 165-byte SPL layout) + 6.1. Validate mint does not have restricted extensions + - Check: `!mint_extensions.has_restricted_extensions()` + - Error: `ErrorCode::MissingCompressibleConfig` if mint has restricted extensions + - Rationale: Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) must be marked as compression_only, and that marker is part of the Compressible extension + 6.2. Create ATA PDA with fee_payer paying rent exemption (base 165-byte SPL layout) 7. Initialize token account with is_ata flag set (same as ## 1. create ctoken account step 3.6, but with is_ata=true) **Errors:** @@ -208,4 +231,6 @@ - `ProgramError::MissingRequiredSignature` (error code: 8) - Custom rent_payer is not a signer - `AccountError::InvalidSigner` (error code: 12015) - fee_payer is not a signer - `AccountError::AccountNotMutable` (error code: 12008) - fee_payer or associated_token_account is not mutable - - `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6116) - rent_payment is exactly 1 epoch (see create ctoken account errors) + - `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6101) - rent_payment is exactly 1 epoch (see create ctoken account errors) + - `ErrorCode::AtaRequiresCompressionOnly` (error code: 6152) - compressible ATA must have compression_only set (compression_only == 0 is not allowed) + - `ErrorCode::MissingCompressibleConfig` (error code: 6115) - non-compressible ATA creation attempted for mint with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs index db3261f79a..2a3838b7bf 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs @@ -15,10 +15,10 @@ use spl_pod::solana_msg::msg; use crate::{ compressed_token::mint_action::accounts::MintActionAccounts, - ctoken::create::parse_config_account, shared::{ convert_program_error, create_pda_account::{create_pda_account, verify_pda}, + parse_config_account, }, }; diff --git a/programs/compressed-token/program/src/compressible/claim.rs b/programs/compressed-token/program/src/compressible/claim.rs index b5129926d9..1518884ff5 100644 --- a/programs/compressed-token/program/src/compressible/claim.rs +++ b/programs/compressed-token/program/src/compressible/claim.rs @@ -10,10 +10,7 @@ use light_program_profiler::profile; use pinocchio::{account_info::AccountInfo, sysvars::Sysvar}; use spl_pod::solana_msg::msg; -use crate::{ - ctoken::create::parse_config_account, - shared::{convert_program_error, transfer_lamports}, -}; +use crate::shared::{convert_program_error, parse_config_account, transfer_lamports}; /// Accounts required for the claim instruction pub struct ClaimAccounts<'a> { diff --git a/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs b/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs index 8cefc06d08..30c47200a3 100644 --- a/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs +++ b/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs @@ -8,7 +8,7 @@ use pinocchio::{ use pinocchio_system::instructions::Transfer; use spl_pod::solana_msg::msg; -use crate::ctoken::create::parse_config_account; +use crate::shared::parse_config_account; /// Accounts required for the withdraw funding pool instruction pub struct WithdrawFundingPoolAccounts<'a> { diff --git a/programs/compressed-token/program/src/ctoken/create.rs b/programs/compressed-token/program/src/ctoken/create.rs index 54a33df1d3..6bd6a3d473 100644 --- a/programs/compressed-token/program/src/ctoken/create.rs +++ b/programs/compressed-token/program/src/ctoken/create.rs @@ -1,132 +1,27 @@ -use anchor_lang::{prelude::ProgramError, pubkey}; +use anchor_lang::prelude::ProgramError; use borsh::BorshDeserialize; -use light_account_checks::{ - checks::{check_discriminator, check_owner}, - AccountIterator, -}; -use light_compressible::config::CompressibleConfig; +use light_account_checks::AccountIterator; +use light_compressed_account::Pubkey; use light_ctoken_interface::instructions::create_ctoken_account::CreateTokenAccountInstructionData; use light_program_profiler::profile; -use pinocchio::{account_info::AccountInfo, instruction::Seed}; -use spl_pod::{bytemuck, solana_msg::msg}; +use pinocchio::account_info::AccountInfo; +use spl_pod::solana_msg::msg; use crate::{ extensions::has_mint_extensions, shared::{ - convert_program_error, create_pda_account, + create_compressible_account, initialize_ctoken_account::{initialize_ctoken_account, CTokenInitConfig}, - transfer_lamports_via_cpi, + next_config_account, }, }; -/// Validated accounts for the create token account instruction -pub struct CreateCTokenAccounts<'info> { - /// The token account being created (signer, mutable) - pub token_account: &'info AccountInfo, - /// The mint for the token account (only used for pubkey not checked) - pub mint: &'info AccountInfo, - /// Optional compressible configuration accounts (None = non-compressible account) - pub compressible: Option>, -} - -/// Accounts required when creating a compressible token account -pub struct CompressibleAccounts<'info> { - /// Pays for the compression incentive rent when rent_payer is the rent recipient (signer, mutable) - pub payer: &'info AccountInfo, - /// Used for account creation CPI - pub system_program: &'info AccountInfo, - /// Either the rent recipient PDA or a custom fee payer - pub rent_payer: &'info AccountInfo, - /// Parsed configuration from the config account - pub parsed_config: &'info CompressibleConfig, -} - -impl<'info> CreateCTokenAccounts<'info> { - /// Parse and validate accounts from the provided account infos - #[profile] - #[inline(always)] - pub fn parse( - account_infos: &'info [AccountInfo], - is_compressible: bool, - ) -> Result { - let mut iter = AccountIterator::new(account_infos); - - // For compressible accounts: token_account must be signer (account created via CPI) - // For non-compressible accounts: token_account doesn't need to be signer (SPL compatibility) - let token_account = if is_compressible { - iter.next_signer_mut("token_account")? - } else { - iter.next_mut("token_account")? - }; - let mint = iter.next_non_mut("mint")?; - - // Parse optional compressible accounts - let compressible = if is_compressible { - Some(CompressibleAccounts { - payer: iter.next_signer_mut("payer")?, - parsed_config: next_config_account(&mut iter)?, - system_program: iter.next_non_mut("system program")?, - // Must be signer if custom rent payer. - // Rent sponsor is not signer. - rent_payer: iter.next_mut("rent payer")?, - }) - } else { - None - }; - - Ok(Self { - token_account, - mint, - compressible, - }) - } -} - -#[profile] -#[inline(always)] -pub fn parse_config_account( - config_account: &AccountInfo, -) -> Result<&CompressibleConfig, ProgramError> { - // Validate config account owner - check_owner( - &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes(), - config_account, - )?; - // Parse config data - let data = unsafe { config_account.borrow_data_unchecked() }; - check_discriminator::(data)?; - let config = bytemuck::pod_from_bytes::(&data[8..]).map_err(|e| { - msg!("Failed to deserialize CompressibleConfig: {:?}", e); - ProgramError::InvalidAccountData - })?; - - Ok(config) -} - -#[profile] -#[inline(always)] -pub fn next_config_account<'info>( - iter: &mut AccountIterator<'info, AccountInfo>, -) -> Result<&'info CompressibleConfig, ProgramError> { - let config_account = iter.next_non_mut("compressible config")?; - let config = parse_config_account(config_account)?; - - // Validate config is active (only active allowed for account creation) - config.validate_active().map_err(ProgramError::from)?; - - Ok(config) -} - /// Process the create token account instruction #[profile] pub fn process_create_token_account( account_infos: &[AccountInfo], mut instruction_data: &[u8], ) -> Result<(), ProgramError> { - use light_compressed_account::Pubkey; - - use crate::shared::initialize_ctoken_account::CompressibleInitData; - // SPL compatibility: if instruction_data is exactly 32 bytes, treat as owner-only (no compressible config) // This matches SPL Token's initialize_account3 which only sends the owner pubkey let inputs = if instruction_data.len() == 32 { @@ -145,27 +40,29 @@ pub fn process_create_token_account( let is_compressible = inputs.compressible_config.is_some(); // Parse and validate accounts - let accounts = CreateCTokenAccounts::parse(account_infos, is_compressible)?; + let mut iter = AccountIterator::new(account_infos); + + // For compressible accounts: token_account must be signer (account created via CPI) + // For non-compressible accounts: token_account doesn't need to be signer (SPL compatibility) + let token_account = if is_compressible { + iter.next_signer_mut("token_account")? + } else { + iter.next_mut("token_account")? + }; + let mint = iter.next_non_mut("mint")?; // Check which extensions the mint has (single deserialization) - let mint_extensions = has_mint_extensions(accounts.mint)?; + let mint_extensions = has_mint_extensions(mint)?; // Handle compressible vs non-compressible account creation let compressible_init_data = if let Some(ref compressible_config) = inputs.compressible_config { - let compressible = accounts - .compressible - .as_ref() - .ok_or(ProgramError::InvalidAccountData)?; - - // Validate that rent_payment is not exactly 1 epoch (footgun prevention) - if compressible_config.rent_payment == 1 { - msg!("Prefunding for exactly 1 epoch is not allowed. If the account is created near an epoch boundary, it could become immediately compressible. Use 0 or 2+ epochs."); - return Err(anchor_compressed_token::ErrorCode::OneEpochPrefundingNotAllowed.into()); - } + let payer = iter.next_signer_mut("payer")?; + let config_account = next_config_account(&mut iter)?; + let _system_program = iter.next_non_mut("system_program")?; + let rent_payer = iter.next_mut("rent_payer")?; if let Some(compress_to_pubkey) = compressible_config.compress_to_account_pubkey.as_ref() { - // Compress to pubkey specifies compression to account pubkey instead of the owner. - compress_to_pubkey.check_seeds(accounts.token_account.key())?; + compress_to_pubkey.check_seeds(token_account.key())?; } // If restricted extensions exist, compression_only must be set @@ -182,67 +79,16 @@ pub fn process_create_token_account( return Err(anchor_compressed_token::ErrorCode::CompressionOnlyNotAllowed.into()); } - // Calculate account size based on extensions (includes Compressible extension) - let account_size = mint_extensions.calculate_account_size(true)?; - - let config_account = compressible.parsed_config; - let rent = config_account - .rent_config - .get_rent_with_compression_cost(account_size, compressible_config.rent_payment as u64); - let account_size = account_size as usize; - - let custom_rent_payer = - *compressible.rent_payer.key() != config_account.rent_sponsor.to_bytes(); - - // Prevents setting executable accounts as rent_sponsor - if custom_rent_payer && !compressible.rent_payer.is_signer() { - msg!("Custom rent payer must be a signer"); - return Err(ProgramError::MissingRequiredSignature); - } - - // Build fee_payer seeds (rent_sponsor PDA or None for custom keypair) - let version_bytes = config_account.version.to_le_bytes(); - let bump_seed = [config_account.rent_sponsor_bump]; - let rent_sponsor_seeds = [ - Seed::from(b"rent_sponsor".as_ref()), - Seed::from(version_bytes.as_ref()), - Seed::from(bump_seed.as_ref()), - ]; - - let fee_payer_seeds = if custom_rent_payer { - None - } else { - Some(rent_sponsor_seeds.as_slice()) - }; - - let additional_lamports = if custom_rent_payer { Some(rent) } else { None }; - - // Create token account (handles DoS prevention internally) - create_pda_account( - compressible.rent_payer, - accounts.token_account, - account_size, - fee_payer_seeds, + Some(create_compressible_account( + compressible_config, + &mint_extensions, + config_account, + rent_payer, + token_account, + payer, None, // token_account is keypair signer - additional_lamports, - )?; - - // When using protocol rent sponsor, payer pays the compression incentive - if !custom_rent_payer { - transfer_lamports_via_cpi(rent, compressible.payer, accounts.token_account) - .map_err(convert_program_error)?; - } - - Some(CompressibleInitData { - ix_data: compressible_config, - config_account: compressible.parsed_config, - custom_rent_payer: if custom_rent_payer { - Some(*compressible.rent_payer.key()) - } else { - None - }, - is_ata: false, - }) + false, + )?) } else { // Non-compressible account: token_account must already exist and be owned by our program // This is SPL-compatible initialize_account3 behavior @@ -251,13 +97,12 @@ pub fn process_create_token_account( // Initialize the token account initialize_ctoken_account( - accounts.token_account, + token_account, CTokenInitConfig { - mint: accounts.mint.key(), owner: &inputs.owner.to_bytes(), compressible: compressible_init_data, mint_extensions, - mint_account: accounts.mint, + mint_account: mint, }, ) } diff --git a/programs/compressed-token/program/src/ctoken/create_ata.rs b/programs/compressed-token/program/src/ctoken/create_ata.rs index 4811d6eb4e..c5ce1dc004 100644 --- a/programs/compressed-token/program/src/ctoken/create_ata.rs +++ b/programs/compressed-token/program/src/ctoken/create_ata.rs @@ -6,15 +6,12 @@ use light_program_profiler::profile; use pinocchio::{account_info::AccountInfo, instruction::Seed}; use spl_pod::solana_msg::msg; -use super::create::next_config_account; use crate::{ extensions::has_mint_extensions, shared::{ - convert_program_error, create_pda_account, - initialize_ctoken_account::{ - initialize_ctoken_account, CTokenInitConfig, CompressibleInitData, - }, - transfer_lamports_via_cpi, validate_ata_derivation, + create_compressible_account, create_pda_account, + initialize_ctoken_account::{initialize_ctoken_account, CTokenInitConfig}, + next_config_account, validate_ata_derivation, }, }; @@ -42,7 +39,8 @@ pub fn process_create_associated_token_account_idempotent( /// 2. fee_payer (signer, mut) /// 3. associated_token_account (mut) /// 4. system_program -/// Optional (only when compressible_config is Some): +/// +/// Optional (only when compressible_config is Some): /// 5. compressible_config /// 6. rent_payer #[profile] @@ -92,90 +90,28 @@ fn process_create_associated_token_account_with_mode( // Handle compressible vs non-compressible account creation let compressible = if let Some(compressible_config) = &inputs.compressible_config { - // Validate that rent_payment is not exactly 1 epoch (footgun prevention) - if compressible_config.rent_payment == 1 { - msg!("Prefunding for exactly 1 epoch is not allowed. If the account is created near an epoch boundary, it could become immediately compressible. Use 0 or 2+ epochs."); - return Err(anchor_compressed_token::ErrorCode::OneEpochPrefundingNotAllowed.into()); - } - - // Associated token accounts must not compress to pubkey if compressible_config.compress_to_account_pubkey.is_some() { msg!("Associated token accounts must not compress to pubkey"); return Err(ProgramError::InvalidInstructionData); } - - // Associated token accounts must always be compression_only if compressible_config.compression_only == 0 { msg!("Associated token accounts must have compression_only set"); return Err(anchor_compressed_token::ErrorCode::AtaRequiresCompressionOnly.into()); } - // Parse additional accounts for compressible path let config_account = next_config_account(&mut iter)?; let rent_payer = iter.next_mut("rent_payer")?; - // Calculate account size based on extensions (includes Compressible extension) - let account_size = mint_extensions.calculate_account_size(true)?; - - let rent = config_account - .rent_config - .get_rent_with_compression_cost(account_size, compressible_config.rent_payment as u64); - let account_size = account_size as usize; - - let custom_rent_payer = *rent_payer.key() != config_account.rent_sponsor.to_bytes(); - - // Prevents setting executable accounts as rent_sponsor - if custom_rent_payer && !rent_payer.is_signer() { - msg!("Custom rent payer must be a signer"); - return Err(ProgramError::MissingRequiredSignature); - } - - // Build rent sponsor seeds if using rent sponsor PDA as fee_payer - let version_bytes = config_account.version.to_le_bytes(); - let rent_sponsor_bump = [config_account.rent_sponsor_bump]; - let rent_sponsor_seeds = [ - Seed::from(b"rent_sponsor".as_ref()), - Seed::from(version_bytes.as_ref()), - Seed::from(rent_sponsor_bump.as_ref()), - ]; - - let fee_payer_seeds = if custom_rent_payer { - None - } else { - Some(rent_sponsor_seeds.as_slice()) - }; - - let additional_lamports = if custom_rent_payer { Some(rent) } else { None }; - - // Create ATA account - create_pda_account( + Some(create_compressible_account( + compressible_config, + &mint_extensions, + config_account, rent_payer, associated_token_account, - account_size, - fee_payer_seeds, - Some(ata_seeds.as_slice()), - additional_lamports, - )?; - - // When using protocol rent sponsor, fee_payer pays the compression incentive - if !custom_rent_payer { - transfer_lamports_via_cpi(rent, fee_payer, associated_token_account) - .map_err(convert_program_error)?; - } - - // For ATAs, we use is_ata flag in the extension instead of compress_to_pubkey. - // The is_ata flag allows decompress to verify the destination is the correct ATA - // while keeping the compressed account owner as the wallet owner (who can sign). - Some(CompressibleInitData { - ix_data: compressible_config, - config_account, - custom_rent_payer: if custom_rent_payer { - Some(*rent_payer.key()) - } else { - None - }, - is_ata: true, - }) + fee_payer, + Some(ata_seeds.as_slice()), // ATA is a PDA + true, // is_ata = true + )?) } else { // Non-compressible path: fee_payer pays for account creation directly // Non-compressible accounts have no extensions (base 165-byte SPL layout) @@ -197,7 +133,6 @@ fn process_create_associated_token_account_with_mode( initialize_ctoken_account( associated_token_account, CTokenInitConfig { - mint: mint_bytes, owner: owner_bytes, compressible, mint_extensions, diff --git a/programs/compressed-token/program/src/shared/config_account.rs b/programs/compressed-token/program/src/shared/config_account.rs new file mode 100644 index 0000000000..d29154d982 --- /dev/null +++ b/programs/compressed-token/program/src/shared/config_account.rs @@ -0,0 +1,44 @@ +use anchor_lang::{prelude::ProgramError, pubkey}; +use light_account_checks::{ + checks::{check_discriminator, check_owner}, + AccountIterator, +}; +use light_compressible::config::CompressibleConfig; +use light_program_profiler::profile; +use pinocchio::account_info::AccountInfo; +use spl_pod::{bytemuck, solana_msg::msg}; + +#[profile] +#[inline(always)] +pub fn parse_config_account( + config_account: &AccountInfo, +) -> Result<&CompressibleConfig, ProgramError> { + // Validate config account owner + check_owner( + &pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX").to_bytes(), + config_account, + )?; + // Parse config data + let data = unsafe { config_account.borrow_data_unchecked() }; + check_discriminator::(data)?; + let config = bytemuck::pod_from_bytes::(&data[8..]).map_err(|e| { + msg!("Failed to deserialize CompressibleConfig: {:?}", e); + ProgramError::InvalidAccountData + })?; + + Ok(config) +} + +#[profile] +#[inline(always)] +pub fn next_config_account<'info>( + iter: &mut AccountIterator<'info, AccountInfo>, +) -> Result<&'info CompressibleConfig, ProgramError> { + let config_account = iter.next_non_mut("compressible config")?; + let config = parse_config_account(config_account)?; + + // Validate config is active (only active allowed for account creation) + config.validate_active().map_err(ProgramError::from)?; + + Ok(config) +} diff --git a/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs b/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs index 78b079a6ac..162a6fd755 100644 --- a/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs +++ b/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs @@ -13,9 +13,12 @@ use light_program_profiler::profile; use light_zero_copy::traits::ZeroCopyNew; #[cfg(target_os = "solana")] use pinocchio::sysvars::{clock::Clock, Sysvar}; -use pinocchio::{account_info::AccountInfo, msg, pubkey::Pubkey}; +use pinocchio::{account_info::AccountInfo, instruction::Seed, msg, pubkey::Pubkey}; -use crate::extensions::MintExtensionFlags; +use crate::{ + extensions::MintExtensionFlags, + shared::{convert_program_error, create_pda_account, transfer_lamports_via_cpi}, +}; const SPL_TOKEN_ID: [u8; 32] = spl_token::ID.to_bytes(); const SPL_TOKEN_2022_ID: [u8; 32] = spl_token_2022::ID.to_bytes(); @@ -43,8 +46,6 @@ pub struct CompressibleInitData<'a> { /// Configuration for initializing a CToken account pub struct CTokenInitConfig<'a> { - /// The mint pubkey (32 bytes) - pub mint: &'a [u8; 32], /// The owner pubkey (32 bytes) pub owner: &'a [u8; 32], /// Compressible configuration (None = not compressible) @@ -55,6 +56,87 @@ pub struct CTokenInitConfig<'a> { pub mint_account: &'a AccountInfo, } +#[profile] +#[inline(always)] +#[allow(clippy::too_many_arguments)] +pub fn create_compressible_account<'info>( + compressible_config: &'info CompressibleExtensionInstructionData, + mint_extensions: &MintExtensionFlags, + config_account: &'info CompressibleConfig, + rent_payer: &'info AccountInfo, + target_account: &'info AccountInfo, + fee_payer: &'info AccountInfo, + account_seeds: Option<&[Seed]>, + is_ata: bool, +) -> Result, ProgramError> { + // Validate rent_payment != 1 (epoch boundary edge case) + if compressible_config.rent_payment == 1 { + msg!("Prefunding for exactly 1 epoch is not allowed. If the account is created near an epoch boundary, it could become immediately compressible. Use 0 or 2+ epochs."); + return Err(anchor_compressed_token::ErrorCode::OneEpochPrefundingNotAllowed.into()); + } + + // Calculate account size (includes Compressible extension) + let account_size = mint_extensions.calculate_account_size(true)?; + + // Calculate rent with compression cost + let rent = config_account + .rent_config + .get_rent_with_compression_cost(account_size, compressible_config.rent_payment as u64); + let account_size = account_size as usize; + + let custom_rent_payer = *rent_payer.key() != config_account.rent_sponsor.to_bytes(); + + // Custom rent payer must be a signer (prevents executable accounts as rent_sponsor) + if custom_rent_payer && !rent_payer.is_signer() { + msg!("Custom rent payer must be a signer"); + return Err(ProgramError::MissingRequiredSignature); + } + + // Build rent sponsor seeds for PDA signing + let version_bytes = config_account.version.to_le_bytes(); + let bump_seed = [config_account.rent_sponsor_bump]; + let rent_sponsor_seeds = [ + Seed::from(b"rent_sponsor".as_ref()), + Seed::from(version_bytes.as_ref()), + Seed::from(bump_seed.as_ref()), + ]; + + let fee_payer_seeds = if custom_rent_payer { + None + } else { + Some(rent_sponsor_seeds.as_slice()) + }; + + let additional_lamports = if custom_rent_payer { Some(rent) } else { None }; + + // Create the account + create_pda_account( + rent_payer, + target_account, + account_size, + fee_payer_seeds, + account_seeds, + additional_lamports, + )?; + + // When using protocol rent sponsor, fee_payer pays the compression incentive + if !custom_rent_payer { + transfer_lamports_via_cpi(rent, fee_payer, target_account) + .map_err(convert_program_error)?; + } + + Ok(CompressibleInitData { + ix_data: compressible_config, + config_account, + custom_rent_payer: if custom_rent_payer { + Some(*rent_payer.key()) + } else { + None + }, + is_ata, + }) +} + /// Initialize a token account using zero-copy with embedded CompressionInfo #[profile] pub fn initialize_ctoken_account( @@ -62,7 +144,6 @@ pub fn initialize_ctoken_account( config: CTokenInitConfig<'_>, ) -> Result<(), ProgramError> { let CTokenInitConfig { - mint, owner, compressible, mint_extensions, @@ -72,19 +153,6 @@ pub fn initialize_ctoken_account( // Build extensions Vec from boolean flags // +1 for potential Compressible extension let mut extensions = Vec::with_capacity(mint_extensions.num_extensions() + 1); - if mint_extensions.has_pausable { - extensions.push(ExtensionStructConfig::PausableAccount(())); - } - if mint_extensions.has_permanent_delegate { - extensions.push(ExtensionStructConfig::PermanentDelegateAccount(())); - } - if mint_extensions.has_transfer_fee { - extensions.push(ExtensionStructConfig::TransferFeeAccount(())); - } - if mint_extensions.has_transfer_hook { - extensions.push(ExtensionStructConfig::TransferHookAccount(())); - } - // Add Compressible extension if compression is enabled if compressible.is_some() { extensions.push(ExtensionStructConfig::Compressible( @@ -92,11 +160,26 @@ pub fn initialize_ctoken_account( info: CompressionInfoConfig { rent_config: () }, }, )); - } + if mint_extensions.has_pausable { + extensions.push(ExtensionStructConfig::PausableAccount(())); + } + if mint_extensions.has_permanent_delegate { + extensions.push(ExtensionStructConfig::PermanentDelegateAccount(())); + } + if mint_extensions.has_transfer_fee { + extensions.push(ExtensionStructConfig::TransferFeeAccount(())); + } + if mint_extensions.has_transfer_hook { + extensions.push(ExtensionStructConfig::TransferHookAccount(())); + } + } else if mint_extensions.has_restricted_extensions() { + // Mints with restricted extensions must have the compressible extension. + return Err(anchor_compressed_token::ErrorCode::MissingCompressibleConfig.into()); + } // Build the config for new_zero_copy let zc_config = CompressedTokenConfig { - mint: light_compressed_account::Pubkey::from(*mint), + mint: light_compressed_account::Pubkey::from(*mint_account.key()), owner: light_compressed_account::Pubkey::from(*owner), state: if mint_extensions.default_state_frozen { AccountState::Frozen as u8 @@ -218,32 +301,39 @@ fn configure_compression_info( // Only try to read decimals if mint has data (is initialized) if !mint_data.is_empty() { let owner = mint_account.owner(); + let decimals = mint_data.get(44); - // Validate mint account based on owner program - let is_valid_mint = if *owner == SPL_TOKEN_ID { - // SPL Token: mint must be exactly 82 bytes - mint_data.len() == SPL_MINT_LEN - } else if *owner == SPL_TOKEN_2022_ID || *owner == CTOKEN_PROGRAM_ID { - // Token-2022/CToken: Either exactly 82 bytes (no extensions) or - // check AccountType marker at offset 165 (with extensions) - // Layout with extensions: 82 bytes mint + 83 bytes padding + AccountType - mint_data.len() == SPL_MINT_LEN - || (mint_data.len() > T22_ACCOUNT_TYPE_OFFSET - && mint_data[T22_ACCOUNT_TYPE_OFFSET] == ACCOUNT_TYPE_MINT) - } else { - msg!("Invalid mint owner"); - return Err(ProgramError::IncorrectProgramId); - }; - - if !is_valid_mint { + if !is_valid_mint(owner, &mint_data)? { msg!("Invalid mint account: not a valid mint"); return Err(ProgramError::InvalidAccountData); } // Mint layout: decimals at byte 44 for all token programs // (mint_authority option: 36, supply: 8) = 44 - compressible_ext.set_decimals(Some(mint_data[44])); + compressible_ext.set_decimals(decimals.copied()); } Ok(()) } + +#[inline(always)] +pub fn is_valid_mint(owner: &Pubkey, mint_data: &[u8]) -> Result { + if *owner == SPL_TOKEN_ID { + // SPL Token: mint must be exactly 82 bytes + Ok(mint_data.len() == SPL_MINT_LEN) + } else if *owner == SPL_TOKEN_2022_ID { + // Token-2022: Either exactly 82 bytes (no extensions) or + // check AccountType marker at offset 165 (with extensions) + // Layout with extensions: 82 bytes mint + 83 bytes padding + AccountType + Ok(mint_data.len() == SPL_MINT_LEN + || (mint_data.len() > T22_ACCOUNT_TYPE_OFFSET + && mint_data[T22_ACCOUNT_TYPE_OFFSET] == ACCOUNT_TYPE_MINT)) + } else if *owner == CTOKEN_PROGRAM_ID { + // CToken: Always has extensions, must be >165 bytes with AccountType=Mint + Ok(mint_data.len() > T22_ACCOUNT_TYPE_OFFSET + && mint_data[T22_ACCOUNT_TYPE_OFFSET] == ACCOUNT_TYPE_MINT) + } else { + msg!("Invalid mint owner"); + Err(ProgramError::IncorrectProgramId) + } +} diff --git a/programs/compressed-token/program/src/shared/mod.rs b/programs/compressed-token/program/src/shared/mod.rs index 99368379a9..3f52465d75 100644 --- a/programs/compressed-token/program/src/shared/mod.rs +++ b/programs/compressed-token/program/src/shared/mod.rs @@ -1,5 +1,6 @@ pub mod accounts; pub mod compressible_top_up; +pub mod config_account; mod convert_program_error; pub mod cpi; pub mod cpi_bytes_size; @@ -12,8 +13,10 @@ pub mod token_output; pub mod transfer_lamports; pub mod validate_ata_derivation; +pub use config_account::{next_config_account, parse_config_account}; pub use convert_program_error::convert_program_error; pub use create_pda_account::{create_pda_account, verify_pda}; +pub use initialize_ctoken_account::create_compressible_account; pub use light_account_checks::AccountIterator; pub use mint_to_token_pool::mint_to_token_pool; pub use transfer_lamports::*; diff --git a/programs/compressed-token/program/tests/check_authority.rs b/programs/compressed-token/program/tests/check_authority.rs index 31e850eeb6..08e9f3624a 100644 --- a/programs/compressed-token/program/tests/check_authority.rs +++ b/programs/compressed-token/program/tests/check_authority.rs @@ -1,6 +1,6 @@ use anchor_compressed_token::ErrorCode; use light_account_checks::account_info::test_account_info::pinocchio::get_account_info; -use light_compressed_token::mint_action::check_authority; +use light_compressed_token::compressed_token::mint_action::check_authority; use pinocchio::pubkey::Pubkey; // Anchor custom error codes start at offset 6000 diff --git a/programs/compressed-token/program/tests/compress_and_close.rs b/programs/compressed-token/program/tests/compress_and_close.rs index 8f7464c508..80e2202973 100644 --- a/programs/compressed-token/program/tests/compress_and_close.rs +++ b/programs/compressed-token/program/tests/compress_and_close.rs @@ -4,7 +4,7 @@ use light_account_checks::{ account_info::test_account_info::pinocchio::get_account_info, packed_accounts::ProgramPackedAccounts, }; -use light_compressed_token::transfer2::{ +use light_compressed_token::compressed_token::transfer2::{ accounts::Transfer2Accounts, compression::ctoken::close_for_compress_and_close, }; use light_ctoken_interface::{ diff --git a/programs/compressed-token/program/tests/mint.rs b/programs/compressed-token/program/tests/mint.rs index 7ed1a09028..6cd0030a21 100644 --- a/programs/compressed-token/program/tests/mint.rs +++ b/programs/compressed-token/program/tests/mint.rs @@ -4,11 +4,11 @@ use light_compressed_account::{ Pubkey, }; use light_compressed_token::{ - constants::COMPRESSED_MINT_DISCRIMINATOR, - mint_action::{ + compressed_token::mint_action::{ accounts::AccountsConfig, mint_input::create_input_compressed_mint_account, zero_copy_config::get_zero_copy_configs, }, + constants::COMPRESSED_MINT_DISCRIMINATOR, }; use light_ctoken_interface::{ instructions::{ diff --git a/programs/compressed-token/program/tests/mint_action.rs b/programs/compressed-token/program/tests/mint_action.rs index 90d9736fb7..666b336c56 100644 --- a/programs/compressed-token/program/tests/mint_action.rs +++ b/programs/compressed-token/program/tests/mint_action.rs @@ -4,7 +4,7 @@ /// that the derived configuration matches expected values based on instruction content. use borsh::BorshSerialize; use light_compressed_account::{instruction_data::compressed_proof::CompressedProof, Pubkey}; -use light_compressed_token::mint_action::accounts::AccountsConfig; +use light_compressed_token::compressed_token::mint_action::accounts::AccountsConfig; use light_ctoken_interface::{ instructions::{ extensions::{token_metadata::TokenMetadataInstructionData, ExtensionInstructionData}, diff --git a/programs/compressed-token/program/tests/mint_validation.rs b/programs/compressed-token/program/tests/mint_validation.rs new file mode 100644 index 0000000000..28bb2c5f62 --- /dev/null +++ b/programs/compressed-token/program/tests/mint_validation.rs @@ -0,0 +1,358 @@ +use anchor_lang::prelude::ProgramError; +use light_compressed_token::shared::initialize_ctoken_account::is_valid_mint; +use pinocchio::pubkey::Pubkey; + +const SPL_TOKEN_ID: Pubkey = spl_token::ID.to_bytes(); +const SPL_TOKEN_2022_ID: Pubkey = spl_token_2022::ID.to_bytes(); +const CTOKEN_PROGRAM_ID: Pubkey = light_ctoken_interface::CTOKEN_PROGRAM_ID; +const SYSTEM_PROGRAM_ID: Pubkey = [0u8; 32]; +const RANDOM_PROGRAM_ID: Pubkey = [42u8; 32]; + +const ACCOUNT_TYPE_UNINITIALIZED: u8 = 0; +const ACCOUNT_TYPE_MINT: u8 = 1; +const ACCOUNT_TYPE_ACCOUNT: u8 = 2; +const ACCOUNT_TYPE_UNKNOWN: u8 = 3; + +/// Owner types for testing +#[derive(Debug, Clone, Copy)] +enum Owner { + SplToken, + Token2022, + CToken, + SystemProgram, + RandomProgram, +} + +impl Owner { + fn pubkey(&self) -> &Pubkey { + match self { + Owner::SplToken => &SPL_TOKEN_ID, + Owner::Token2022 => &SPL_TOKEN_2022_ID, + Owner::CToken => &CTOKEN_PROGRAM_ID, + Owner::SystemProgram => &SYSTEM_PROGRAM_ID, + Owner::RandomProgram => &RANDOM_PROGRAM_ID, + } + } +} + +/// Data configurations for testing +#[derive(Debug, Clone)] +enum MintData { + Empty, + TooSmall(usize), // < 82 bytes + ExactSplSize, // 82 bytes (valid for all) + BetweenSizes(usize), // 83-165 bytes + WithAccountType(u8), // 166+ bytes with specific AccountType +} + +impl MintData { + fn to_bytes(&self) -> Vec { + match self { + MintData::Empty => vec![], + MintData::TooSmall(size) => vec![0u8; *size], + MintData::ExactSplSize => vec![0u8; 82], + MintData::BetweenSizes(size) => vec![0u8; *size], + MintData::WithAccountType(account_type) => { + let mut data = vec![0u8; 170]; + data[165] = *account_type; + data + } + } + } +} + +/// Expected result for a test case +#[derive(Debug, Clone, Copy, PartialEq)] +enum Expected { + Valid, // Ok(true) + Invalid, // Ok(false) + IncorrectProgramId, // Err(IncorrectProgramId) +} + +/// Test case definition +struct TestCase { + owner: Owner, + data: MintData, + expected: Expected, + description: &'static str, +} + +fn run_test_case(tc: &TestCase) { + let data = tc.data.to_bytes(); + let result = is_valid_mint(tc.owner.pubkey(), &data); + + match tc.expected { + Expected::Valid => { + assert!( + result.as_ref().map(|v| *v).unwrap_or(false), + "FAILED: {} - expected Ok(true), got {:?}", + tc.description, + result + ); + } + Expected::Invalid => { + assert!( + result.as_ref().map(|v| !*v).unwrap_or(false), + "FAILED: {} - expected Ok(false), got {:?}", + tc.description, + result + ); + } + Expected::IncorrectProgramId => { + assert!( + result.as_ref().err() == Some(&ProgramError::IncorrectProgramId), + "FAILED: {} - expected Err(IncorrectProgramId), got {:?}", + tc.description, + result + ); + } + } +} + +/// Systematically test all owner x data combinations +#[test] +fn test_is_valid_mint_all_combinations() { + let test_cases = vec![ + // ========================================================================= + // INVALID OWNERS - should always return Err(IncorrectProgramId) + // ========================================================================= + TestCase { + owner: Owner::SystemProgram, + data: MintData::ExactSplSize, + expected: Expected::IncorrectProgramId, + description: "System program owner with 82 bytes", + }, + TestCase { + owner: Owner::RandomProgram, + data: MintData::ExactSplSize, + expected: Expected::IncorrectProgramId, + description: "Random program owner with 82 bytes", + }, + TestCase { + owner: Owner::SystemProgram, + data: MintData::WithAccountType(ACCOUNT_TYPE_MINT), + expected: Expected::IncorrectProgramId, + description: "System program owner with AccountType=Mint", + }, + // ========================================================================= + // SPL TOKEN - only accepts exactly 82 bytes + // ========================================================================= + TestCase { + owner: Owner::SplToken, + data: MintData::Empty, + expected: Expected::Invalid, + description: "SPL: empty data", + }, + TestCase { + owner: Owner::SplToken, + data: MintData::TooSmall(40), + expected: Expected::Invalid, + description: "SPL: 40 bytes (< 82)", + }, + TestCase { + owner: Owner::SplToken, + data: MintData::TooSmall(81), + expected: Expected::Invalid, + description: "SPL: 81 bytes (off by one)", + }, + TestCase { + owner: Owner::SplToken, + data: MintData::ExactSplSize, + expected: Expected::Valid, + description: "SPL: exactly 82 bytes (valid mint)", + }, + TestCase { + owner: Owner::SplToken, + data: MintData::BetweenSizes(83), + expected: Expected::Invalid, + description: "SPL: 83 bytes (off by one, too large)", + }, + TestCase { + owner: Owner::SplToken, + data: MintData::BetweenSizes(165), + expected: Expected::Invalid, + description: "SPL: 165 bytes (token account size)", + }, + TestCase { + owner: Owner::SplToken, + data: MintData::WithAccountType(ACCOUNT_TYPE_MINT), + expected: Expected::Invalid, + description: "SPL: 170 bytes with AccountType=Mint (SPL doesnt support extensions)", + }, + TestCase { + owner: Owner::SplToken, + data: MintData::WithAccountType(ACCOUNT_TYPE_ACCOUNT), + expected: Expected::Invalid, + description: "SPL: 170 bytes with AccountType=Account", + }, + // ========================================================================= + // TOKEN-2022 - accepts 82 bytes OR 166+ with AccountType=Mint + // ========================================================================= + TestCase { + owner: Owner::Token2022, + data: MintData::Empty, + expected: Expected::Invalid, + description: "T22: empty data", + }, + TestCase { + owner: Owner::Token2022, + data: MintData::TooSmall(40), + expected: Expected::Invalid, + description: "T22: 40 bytes (< 82)", + }, + TestCase { + owner: Owner::Token2022, + data: MintData::TooSmall(81), + expected: Expected::Invalid, + description: "T22: 81 bytes (off by one)", + }, + TestCase { + owner: Owner::Token2022, + data: MintData::ExactSplSize, + expected: Expected::Valid, + description: "T22: exactly 82 bytes (valid mint without extensions)", + }, + TestCase { + owner: Owner::Token2022, + data: MintData::BetweenSizes(83), + expected: Expected::Invalid, + description: "T22: 83 bytes (invalid - between sizes)", + }, + TestCase { + owner: Owner::Token2022, + data: MintData::BetweenSizes(165), + expected: Expected::Invalid, + description: "T22: 165 bytes (edge case - no AccountType marker)", + }, + TestCase { + owner: Owner::Token2022, + data: MintData::WithAccountType(ACCOUNT_TYPE_UNINITIALIZED), + expected: Expected::Invalid, + description: "T22: 170 bytes with AccountType=0 (uninitialized)", + }, + TestCase { + owner: Owner::Token2022, + data: MintData::WithAccountType(ACCOUNT_TYPE_MINT), + expected: Expected::Valid, + description: "T22: 170 bytes with AccountType=Mint (valid)", + }, + TestCase { + owner: Owner::Token2022, + data: MintData::WithAccountType(ACCOUNT_TYPE_ACCOUNT), + expected: Expected::Invalid, + description: "T22: 170 bytes with AccountType=Account (token account)", + }, + TestCase { + owner: Owner::Token2022, + data: MintData::WithAccountType(ACCOUNT_TYPE_UNKNOWN), + expected: Expected::Invalid, + description: "T22: 170 bytes with AccountType=3 (unknown)", + }, + TestCase { + owner: Owner::Token2022, + data: MintData::WithAccountType(255), + expected: Expected::Invalid, + description: "T22: 170 bytes with AccountType=255 (invalid)", + }, + // ========================================================================= + // CTOKEN - must always be >165 bytes with AccountType=Mint + // ========================================================================= + TestCase { + owner: Owner::CToken, + data: MintData::Empty, + expected: Expected::Invalid, + description: "CToken: empty data", + }, + TestCase { + owner: Owner::CToken, + data: MintData::TooSmall(40), + expected: Expected::Invalid, + description: "CToken: 40 bytes (< 82)", + }, + TestCase { + owner: Owner::CToken, + data: MintData::TooSmall(81), + expected: Expected::Invalid, + description: "CToken: 81 bytes (off by one)", + }, + TestCase { + owner: Owner::CToken, + data: MintData::ExactSplSize, + expected: Expected::Invalid, + description: "CToken: 82 bytes (invalid - CToken always has extensions)", + }, + TestCase { + owner: Owner::CToken, + data: MintData::BetweenSizes(83), + expected: Expected::Invalid, + description: "CToken: 83 bytes (invalid - between sizes)", + }, + TestCase { + owner: Owner::CToken, + data: MintData::BetweenSizes(165), + expected: Expected::Invalid, + description: "CToken: 165 bytes (edge case - no AccountType marker)", + }, + TestCase { + owner: Owner::CToken, + data: MintData::WithAccountType(ACCOUNT_TYPE_UNINITIALIZED), + expected: Expected::Invalid, + description: "CToken: 170 bytes with AccountType=0 (uninitialized)", + }, + TestCase { + owner: Owner::CToken, + data: MintData::WithAccountType(ACCOUNT_TYPE_MINT), + expected: Expected::Valid, + description: "CToken: 170 bytes with AccountType=Mint (valid)", + }, + TestCase { + owner: Owner::CToken, + data: MintData::WithAccountType(ACCOUNT_TYPE_ACCOUNT), + expected: Expected::Invalid, + description: "CToken: 170 bytes with AccountType=Account (token account)", + }, + TestCase { + owner: Owner::CToken, + data: MintData::WithAccountType(ACCOUNT_TYPE_UNKNOWN), + expected: Expected::Invalid, + description: "CToken: 170 bytes with AccountType=3 (unknown)", + }, + ]; + + println!( + "\nRunning {} test cases for is_valid_mint:\n", + test_cases.len() + ); + + let mut passed = 0; + let mut failed = 0; + + for tc in &test_cases { + print!(" {:60} ... ", tc.description); + let data = tc.data.to_bytes(); + let result = is_valid_mint(tc.owner.pubkey(), &data); + + let success = match tc.expected { + Expected::Valid => result.as_ref().map(|v| *v).unwrap_or(false), + Expected::Invalid => result.as_ref().map(|v| !*v).unwrap_or(false), + Expected::IncorrectProgramId => { + result.as_ref().err() == Some(&ProgramError::IncorrectProgramId) + } + }; + + if success { + println!("ok"); + passed += 1; + } else { + println!("FAILED (got {:?})", result); + failed += 1; + } + } + + println!("\nResults: {} passed, {} failed\n", passed, failed); + + // Now run assertions to fail the test if any failed + for tc in &test_cases { + run_test_case(tc); + } +} diff --git a/programs/compressed-token/program/tests/multi_sum_check.rs b/programs/compressed-token/program/tests/multi_sum_check.rs index d33091d221..96d4ded630 100644 --- a/programs/compressed-token/program/tests/multi_sum_check.rs +++ b/programs/compressed-token/program/tests/multi_sum_check.rs @@ -6,7 +6,7 @@ use light_account_checks::{ account_info::test_account_info::pinocchio::get_account_info, packed_accounts::ProgramPackedAccounts, }; -use light_compressed_token::transfer2::sum_check::{ +use light_compressed_token::compressed_token::transfer2::sum_check::{ sum_check_multi_mint, validate_mint_uniqueness, }; use light_ctoken_interface::instructions::transfer2::{ diff --git a/programs/compressed-token/program/tests/queue_indices.rs b/programs/compressed-token/program/tests/queue_indices.rs index 886d65e6c8..13bfa8731d 100644 --- a/programs/compressed-token/program/tests/queue_indices.rs +++ b/programs/compressed-token/program/tests/queue_indices.rs @@ -1,6 +1,6 @@ use anchor_compressed_token::ErrorCode; use anchor_lang::AnchorSerialize; -use light_compressed_token::mint_action::queue_indices::QueueIndices; +use light_compressed_token::compressed_token::mint_action::queue_indices::QueueIndices; use light_ctoken_interface::instructions::mint_action::CpiContext; use light_zero_copy::traits::ZeroCopyAt; From c4dff3613008c3e8b7eacb998a35fb2885b5614f Mon Sep 17 00:00:00 2001 From: ananas Date: Tue, 6 Jan 2026 00:41:16 +0000 Subject: [PATCH 05/18] fix doc error codes --- .../docs/compressed_token/MINT_ACTION.md | 26 +++++++-------- .../docs/compressed_token/TRANSFER2.md | 32 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md b/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md index e26737a365..3e6c045b87 100644 --- a/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md +++ b/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md @@ -214,19 +214,19 @@ Packed accounts (remaining accounts): - `ProgramError::InvalidInstructionData` (error code: 3) - Failed to deserialize instruction data or invalid action configuration - `ProgramError::InvalidAccountData` (error code: 4) - Account validation failures (wrong program ownership, invalid PDA derivation) - `ProgramError::InvalidArgument` (error code: 1) - Invalid authority or action parameters -- `ErrorCode::MintActionProofMissing` (error code: 6070) - ZK proof required but not provided -- `ErrorCode::InvalidAuthorityMint` (error code: 6076) - Signer doesn't match mint authority -- `ErrorCode::MintActionAmountTooLarge` (error code: 6101) - Arithmetic overflow in mint amount calculations -- `ErrorCode::MintAccountMismatch` (error code: 6102) - SPL mint account doesn't match expected cmint -- `ErrorCode::InvalidAddressTree` (error code: 6069) - Wrong address merkle tree for mint creation -- `ErrorCode::MintActionMissingSplMintSigner` (error code: 6058) - Missing mint signer for SPL mint creation -- `ErrorCode::MintActionMissingMintAccount` (error code: 6061) - Missing SPL mint account when required -- `ErrorCode::MintActionMissingTokenPoolAccount` (error code: 6062) - Missing token pool PDA when required -- `ErrorCode::MintActionMissingTokenProgram` (error code: 6063) - Missing token program when required -- `ErrorCode::MintActionInvalidExtensionIndex` (error code: 6079) - Extension index out of bounds -- `ErrorCode::MintActionInvalidExtensionType` (error code: 6081) - Extension is not TokenMetadata type -- `ErrorCode::MintActionMetadataKeyNotFound` (error code: 6082) - Metadata key not found for removal -- `ErrorCode::MintActionMissingExecutingAccounts` (error code: 6083) - Missing required execution accounts +- `ErrorCode::MintActionProofMissing` (error code: 6055) - ZK proof required but not provided +- `ErrorCode::InvalidAuthorityMint` (error code: 6018) - Signer doesn't match mint authority +- `ErrorCode::MintActionAmountTooLarge` (error code: 6069) - Arithmetic overflow in mint amount calculations +- `ErrorCode::MintAccountMismatch` (error code: 6051) - SPL mint account doesn't match expected cmint +- `ErrorCode::InvalidAddressTree` (error code: 6094) - Wrong address merkle tree for mint creation +- `ErrorCode::MintActionMissingSplMintSigner` (error code: 6045) - Missing mint signer for SPL mint creation +- `ErrorCode::MintActionMissingMintAccount` (error code: 6048) - Missing SPL mint account when required +- `ErrorCode::MintActionMissingTokenPoolAccount` (error code: 6049) - Missing token pool PDA when required +- `ErrorCode::MintActionMissingTokenProgram` (error code: 6050) - Missing token program when required +- `ErrorCode::MintActionInvalidExtensionIndex` (error code: 6059) - Extension index out of bounds +- `ErrorCode::MintActionInvalidExtensionType` (error code: 6062) - Extension is not TokenMetadata type +- `ErrorCode::MintActionMetadataKeyNotFound` (error code: 6063) - Metadata key not found for removal +- `ErrorCode::MintActionMissingExecutingAccounts` (error code: 6064) - Missing required execution accounts - `ErrorCode::CpiContextExpected` (error code: 6085) - CPI context required but not provided - `AccountError::InvalidSigner` (error code: 12015) - Required signer account is not signing - `AccountError::NotEnoughAccountKeys` (error code: 12020) - Missing required accounts diff --git a/programs/compressed-token/program/docs/compressed_token/TRANSFER2.md b/programs/compressed-token/program/docs/compressed_token/TRANSFER2.md index 56a15f97d2..310de0dcd4 100644 --- a/programs/compressed-token/program/docs/compressed_token/TRANSFER2.md +++ b/programs/compressed-token/program/docs/compressed_token/TRANSFER2.md @@ -341,14 +341,14 @@ When compression processing occurs (in both Path A and Path B): - `CTokenError::InsufficientSupply` (error code: 18010) - Insufficient token supply for operation - `CTokenError::ArithmeticOverflow` (error code: 18003) - Arithmetic overflow in balance calculations - `ErrorCode::SumCheckFailed` (error code: 6005) - Input/output token amounts don't match -- `ErrorCode::InputsOutOfOrder` (error code: 6054) - Sum inputs mint indices not in ascending order -- `ErrorCode::TooManyMints` (error code: 6055) - Sum check, too many mints (max 5) -- `ErrorCode::DuplicateMint` (error code: 6056) - Duplicate mint index detected in inputs, outputs, or compressions (same mint referenced by multiple indices or same index used multiple times) +- `ErrorCode::InputsOutOfOrder` (error code: 6038) - Sum inputs mint indices not in ascending order +- `ErrorCode::TooManyMints` (error code: 6039) - Sum check, too many mints (max 5) +- `ErrorCode::DuplicateMint` (error code: 6102) - Duplicate mint index detected in inputs, outputs, or compressions (same mint referenced by multiple indices or same index used multiple times) - `ErrorCode::ComputeOutputSumFailed` (error code: 6002) - Output mint not in inputs or compressions -- `ErrorCode::TooManyCompressionTransfers` (error code: 6106) - Too many compression transfers. Maximum 40 transfers allowed per instruction +- `ErrorCode::TooManyCompressionTransfers` (error code: 6095) - Too many compression transfers. Maximum 40 transfers allowed per instruction - `ErrorCode::NoInputsProvided` (error code: 6025) - No compressions provided in early exit path (no compressed accounts) -- `ErrorCode::CompressionsOnlyMissingFeePayer` (error code: 6026) - Missing fee payer for compressions-only operations -- `ErrorCode::CompressionsOnlyMissingCpiAuthority` (error code: 6027) - Missing CPI authority PDA for compressions-only operations +- `ErrorCode::CompressionsOnlyMissingFeePayer` (error code: 6096) - Missing fee payer for compressions-only operations +- `ErrorCode::CompressionsOnlyMissingCpiAuthority` (error code: 6097) - Missing CPI authority PDA for compressions-only operations - `ErrorCode::OwnerMismatch` (error code: 6075) - Authority doesn't match account owner or delegate - `ErrorCode::Transfer2CpiContextWriteInvalidAccess` (error code: 6082) - Invalid access to system accounts during CPI write - `ErrorCode::Transfer2CpiContextWriteWithSolPool` (error code: 6083) - SOL pool operations not supported with CPI context write @@ -361,16 +361,16 @@ When compression processing occurs (in both Path A and Path B): - `ErrorCode::CompressAndCloseBalanceMismatch` (error code: 6091) - Token account balance must match compressed output amount - `ErrorCode::CompressAndCloseDelegateNotAllowed` (error code: 6092) - Source token account has delegate OR compressed output has delegate (delegates not supported) - `ErrorCode::CompressAndCloseInvalidVersion` (error code: 6093) - Compressed token version must be 3 (ShaFlat) and must match compressible extension's account_version -- `ErrorCode::CompressAndCloseInvalidMint` (error code: 6108) - Compressed token mint does not match source token account mint -- `ErrorCode::CompressAndCloseMissingCompressedOnlyExtension` (error code: 6109) - Missing required CompressedOnly extension for restricted mint or frozen account -- `ErrorCode::CompressAndCloseDelegatedAmountMismatch` (error code: 6116) - Delegated amount mismatch between ctoken and CompressedOnly extension -- `ErrorCode::CompressAndCloseInvalidDelegate` (error code: 6118) - Delegate mismatch between ctoken and compressed token output -- `ErrorCode::CompressAndCloseWithheldFeeMismatch` (error code: 6120) - Withheld transfer fee mismatch -- `ErrorCode::CompressAndCloseFrozenMismatch` (error code: 6122) - Frozen state mismatch between ctoken and CompressedOnly extension -- `ErrorCode::CompressedOnlyRequiresCTokenDecompress` (error code: 6144) - CompressedOnly inputs must decompress to CToken account, not SPL token account -- `ErrorCode::TlvRequiresVersion3` (error code: 6123) - TLV extensions only supported with version 3 (ShaFlat) -- `ErrorCode::CompressAndCloseDuplicateOutput` (error code: 6420) - Cannot use the same compressed output account for multiple CompressAndClose operations (security protection against fund theft) -- `ErrorCode::CompressAndCloseOutputMissing` (error code: 6421) - Compressed token account output required but not provided +- `ErrorCode::CompressAndCloseInvalidMint` (error code: 6132) - Compressed token mint does not match source token account mint +- `ErrorCode::CompressAndCloseMissingCompressedOnlyExtension` (error code: 6133) - Missing required CompressedOnly extension for restricted mint or frozen account +- `ErrorCode::CompressAndCloseDelegatedAmountMismatch` (error code: 6135) - Delegated amount mismatch between ctoken and CompressedOnly extension +- `ErrorCode::CompressAndCloseInvalidDelegate` (error code: 6136) - Delegate mismatch between ctoken and compressed token output +- `ErrorCode::CompressAndCloseWithheldFeeMismatch` (error code: 6137) - Withheld transfer fee mismatch +- `ErrorCode::CompressAndCloseFrozenMismatch` (error code: 6138) - Frozen state mismatch between ctoken and CompressedOnly extension +- `ErrorCode::CompressedOnlyRequiresCTokenDecompress` (error code: 6149) - CompressedOnly inputs must decompress to CToken account, not SPL token account +- `ErrorCode::TlvRequiresVersion3` (error code: 6139) - TLV extensions only supported with version 3 (ShaFlat) +- `ErrorCode::CompressAndCloseDuplicateOutput` (error code: 6106) - Cannot use the same compressed output account for multiple CompressAndClose operations (security protection against fund theft) +- `ErrorCode::CompressAndCloseOutputMissing` (error code: 6107) - Compressed token account output required but not provided - `AccountError::InvalidSigner` (error code: 12015) - Required signer account is not signing - `AccountError::AccountNotMutable` (error code: 12008) - Required mutable account is not mutable - Additional errors from close_token_account for CompressAndClose operations From 9db54e815d957e4a96df5050030e460d303d6050 Mon Sep 17 00:00:00 2001 From: ananas Date: Tue, 6 Jan 2026 20:30:48 +0000 Subject: [PATCH 06/18] refactor: unify process_compression_top_up with CalculateTopUp trait - Add CalculateTopUp trait for generic top-up calculations - Implement trait for CompressionInfo, ZCompressionInfo, ZCompressionInfoMut - Unify process_compression_top_up to use shared implementation - Remove duplicate function from compress_or_decompress_ctokens.rs - Fix extension ordering in assert_create_token_account (insert at index 0) --- .../compressible/src/compression_info.rs | 40 ++++++++ .../utils/src/assert_create_token_account.rs | 4 +- .../ctoken/compress_or_decompress_ctokens.rs | 49 ++-------- .../transfer2/compression/ctoken/mod.rs | 4 +- .../program/src/ctoken/approve_revoke.rs | 20 ++-- .../program/src/shared/compressible_top_up.rs | 95 ++++++++++++------- 6 files changed, 115 insertions(+), 97 deletions(-) diff --git a/program-libs/compressible/src/compression_info.rs b/program-libs/compressible/src/compression_info.rs index 4bb20e196b..cd46ceff75 100644 --- a/program-libs/compressible/src/compression_info.rs +++ b/program-libs/compressible/src/compression_info.rs @@ -15,6 +15,17 @@ use crate::{ AnchorDeserialize, AnchorSerialize, }; +/// Trait for types that can calculate top-up lamports for compressible accounts. +pub trait CalculateTopUp { + fn calculate_top_up_lamports( + &self, + num_bytes: u64, + current_slot: u64, + current_lamports: u64, + rent_exemption_lamports: u64, + ) -> Result; +} + /// Compressible extension for ctoken accounts. #[derive( Debug, @@ -129,6 +140,35 @@ impl_is_compressible!(CompressionInfo); impl_is_compressible!(ZCompressionInfo<'_>); impl_is_compressible!(ZCompressionInfoMut<'_>); +// Implement CalculateTopUp trait for all compressible extension types +macro_rules! impl_calculate_top_up { + ($struct_name:ty) => { + impl CalculateTopUp for $struct_name { + #[inline(always)] + fn calculate_top_up_lamports( + &self, + num_bytes: u64, + current_slot: u64, + current_lamports: u64, + rent_exemption_lamports: u64, + ) -> Result { + // Delegate to the inherent method + Self::calculate_top_up_lamports( + self, + num_bytes, + current_slot, + current_lamports, + rent_exemption_lamports, + ) + } + } + }; +} + +impl_calculate_top_up!(CompressionInfo); +impl_calculate_top_up!(ZCompressionInfo<'_>); +impl_calculate_top_up!(ZCompressionInfoMut<'_>); + // Unified macro to implement get_last_funded_epoch for all extension types macro_rules! impl_get_last_paid_epoch { ($struct_name:ty) => { diff --git a/program-tests/utils/src/assert_create_token_account.rs b/program-tests/utils/src/assert_create_token_account.rs index ad31b35957..014ad00484 100644 --- a/program-tests/utils/src/assert_create_token_account.rs +++ b/program-tests/utils/src/assert_create_token_account.rs @@ -214,9 +214,9 @@ pub async fn assert_create_token_account_internal( }, }; - // Add Compressible extension to extensions list + // Add Compressible extension to extensions list (at beginning, matching program order) let mut all_extensions = final_extensions.unwrap_or_default(); - all_extensions.push(ExtensionStruct::Compressible(compressible_ext)); + all_extensions.insert(0, ExtensionStruct::Compressible(compressible_ext)); // Create expected compressible token account with embedded compression info let expected_token_account = CToken { diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs index 51a6b44029..1c5f61f478 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs @@ -8,18 +8,16 @@ use light_ctoken_interface::{ }; use light_program_profiler::profile; use light_zero_copy::traits::ZeroCopyAtMut; -use pinocchio::{ - account_info::AccountInfo, - pubkey::pubkey_eq, - sysvars::{clock::Clock, rent::Rent, Sysvar}, -}; +use pinocchio::pubkey::pubkey_eq; use spl_pod::solana_msg::msg; use super::{ compress_and_close::process_compress_and_close, decompress::apply_decompress_extension_state, inputs::CTokenCompressionInputs, }; -use crate::shared::owner_validation::check_ctoken_owner; +use crate::shared::{ + compressible_top_up::process_compression_top_up, owner_validation::check_ctoken_owner, +}; /// Perform compression/decompression on a ctoken account. /// @@ -78,6 +76,7 @@ pub fn compress_or_decompress_ctokens( &mut current_slot, transfer_amount, lamports_budget, + &mut None, )?; } Ok(()) @@ -100,6 +99,7 @@ pub fn compress_or_decompress_ctokens( &mut current_slot, transfer_amount, lamports_budget, + &mut None, )?; } Ok(()) @@ -115,43 +115,6 @@ pub fn compress_or_decompress_ctokens( } } -/// Process compression top-up using embedded compression info. -/// All ctoken accounts now have compression info embedded directly in meta. -#[inline(always)] -pub fn process_compression_top_up( - compression: &light_compressible::compression_info::ZCompressionInfoMut<'_>, - token_account_info: &AccountInfo, - current_slot: &mut u64, - transfer_amount: &mut u64, - lamports_budget: &mut u64, -) -> Result<(), ProgramError> { - if *transfer_amount != 0 { - return Ok(()); - } - - if *current_slot == 0 { - *current_slot = Clock::get() - .map_err(|_| CTokenError::SysvarAccessError)? - .slot; - } - let rent_exemption = Rent::get() - .map_err(|_| CTokenError::SysvarAccessError)? - .minimum_balance(token_account_info.data_len()); - - *transfer_amount = compression - .calculate_top_up_lamports( - token_account_info.data_len() as u64, - *current_slot, - token_account_info.lamports(), - rent_exemption, - ) - .map_err(|_| CTokenError::InvalidAccountData)?; - - *lamports_budget = lamports_budget.saturating_sub(*transfer_amount); - - Ok(()) -} - /// Validate a CToken account for compression/decompression operations. /// /// Checks: diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/mod.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/mod.rs index b52ba4a2d6..f268e7791e 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/mod.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/mod.rs @@ -14,9 +14,7 @@ mod decompress; mod inputs; pub use compress_and_close::close_for_compress_and_close; -pub use compress_or_decompress_ctokens::{ - compress_or_decompress_ctokens, process_compression_top_up, -}; +pub use compress_or_decompress_ctokens::compress_or_decompress_ctokens; pub use inputs::{CTokenCompressionInputs, CompressAndCloseInputs, DecompressCompressOnlyInputs}; /// Process compression/decompression for ctoken accounts. diff --git a/programs/compressed-token/program/src/ctoken/approve_revoke.rs b/programs/compressed-token/program/src/ctoken/approve_revoke.rs index cb54ec26b1..1907ec697a 100644 --- a/programs/compressed-token/program/src/ctoken/approve_revoke.rs +++ b/programs/compressed-token/program/src/ctoken/approve_revoke.rs @@ -6,12 +6,9 @@ use pinocchio_token_program::processor::{ shared::approve::process_approve as shared_process_approve, unpack_amount_and_decimals, }; -use crate::{ - compressed_token::transfer2::compression::ctoken::process_compression_top_up, - shared::{ - convert_program_error, owner_validation::check_token_program_owner, - transfer_lamports_via_cpi, - }, +use crate::shared::{ + compressible_top_up::process_compression_top_up, convert_program_error, + owner_validation::check_token_program_owner, transfer_lamports_via_cpi, }; /// Account indices for approve instruction @@ -124,21 +121,17 @@ fn process_compressible_top_up( // Only process top-up if account has Compressible extension let transfer_amount = if let Some(compressible) = ctoken.get_compressible_extension() { let mut transfer_amount = 0u64; - let mut lamports_budget = if max_top_up == 0 { - u64::MAX - } else { - (max_top_up as u64).saturating_add(1) - }; process_compression_top_up( &compressible.info, account, &mut 0, &mut transfer_amount, - &mut lamports_budget, + &mut 0, + &mut None, )?; - if transfer_amount > 0 && lamports_budget == 0 { + if max_top_up > 0 && (max_top_up as u64) < transfer_amount { return Err(CTokenError::MaxTopUpExceeded.into()); } transfer_amount @@ -248,6 +241,7 @@ pub fn process_ctoken_approve_checked( &mut 0, &mut transfer_amount, &mut lamports_budget, + &mut None, )?; if transfer_amount > 0 && lamports_budget == 0 { diff --git a/programs/compressed-token/program/src/shared/compressible_top_up.rs b/programs/compressed-token/program/src/shared/compressible_top_up.rs index 43af640e6e..1f4531aebd 100644 --- a/programs/compressed-token/program/src/shared/compressible_top_up.rs +++ b/programs/compressed-token/program/src/shared/compressible_top_up.rs @@ -43,6 +43,7 @@ pub fn calculate_and_execute_compressible_top_ups<'a>( let mut current_slot = 0; let mut rent: Option = None; + // Initialize budget: +1 allows exact match (total == max_top_up) let mut lamports_budget = (max_top_up as u64).saturating_add(1); @@ -51,25 +52,14 @@ pub fn calculate_and_execute_compressible_top_ups<'a>( let cmint_data = cmint.try_borrow_data().map_err(convert_program_error)?; let (mint, _) = CompressedMint::zero_copy_at_checked(&cmint_data) .map_err(|_| CTokenError::CMintDeserializationFailed)?; - // Access compression info directly from meta (all cmints now have compression embedded) - if current_slot == 0 { - current_slot = Clock::get() - .map_err(|_| CTokenError::SysvarAccessError)? - .slot; - rent = Some(Rent::get().map_err(|_| CTokenError::SysvarAccessError)?); - } - let rent_exemption = rent.as_ref().unwrap().minimum_balance(cmint.data_len()); - transfers[0].amount = mint - .base - .compression - .calculate_top_up_lamports( - cmint.data_len() as u64, - current_slot, - cmint.lamports(), - rent_exemption, - ) - .map_err(|_| CTokenError::InvalidAccountData)?; - lamports_budget = lamports_budget.saturating_sub(transfers[0].amount); + process_compression_top_up( + &mint.base.compression, + cmint, + &mut current_slot, + &mut transfers[0].amount, + &mut lamports_budget, + &mut rent, + )?; } // Calculate CToken top-up (only if not 165 bytes - 165 means no extensions) @@ -80,23 +70,14 @@ pub fn calculate_and_execute_compressible_top_ups<'a>( let compressible = token .get_compressible_extension() .ok_or::(CTokenError::MissingCompressibleExtension.into())?; - if current_slot == 0 { - current_slot = Clock::get() - .map_err(|_| CTokenError::SysvarAccessError)? - .slot; - rent = Some(Rent::get().map_err(|_| CTokenError::SysvarAccessError)?); - } - let rent_exemption = rent.as_ref().unwrap().minimum_balance(ctoken.data_len()); - transfers[1].amount = compressible - .info - .calculate_top_up_lamports( - ctoken.data_len() as u64, - current_slot, - ctoken.lamports(), - rent_exemption, - ) - .map_err(|_| CTokenError::InvalidAccountData)?; - lamports_budget = lamports_budget.saturating_sub(transfers[1].amount); + process_compression_top_up( + &compressible.info, + ctoken, + &mut current_slot, + &mut transfers[1].amount, + &mut lamports_budget, + &mut rent, + )?; } // Exit early if no compressible accounts @@ -116,3 +97,45 @@ pub fn calculate_and_execute_compressible_top_ups<'a>( multi_transfer_lamports(payer, &transfers).map_err(convert_program_error)?; Ok(()) } + +/// Process compression top-up using embedded compression info. +/// All ctoken accounts now have compression info embedded directly in meta. +#[inline(always)] +pub fn process_compression_top_up( + compression: &T, + account_info: &AccountInfo, + current_slot: &mut u64, + transfer_amount: &mut u64, + lamports_budget: &mut u64, + rent: &mut Option, +) -> Result<(), ProgramError> { + if *transfer_amount != 0 { + return Ok(()); + } + + if *current_slot == 0 { + *current_slot = Clock::get() + .map_err(|_| CTokenError::SysvarAccessError)? + .slot; + } + if rent.is_none() { + *rent = Some(Rent::get().map_err(|_| CTokenError::SysvarAccessError)?); + } + let rent_exemption = rent + .as_ref() + .unwrap() + .minimum_balance(account_info.data_len()); + + *transfer_amount = compression + .calculate_top_up_lamports( + account_info.data_len() as u64, + *current_slot, + account_info.lamports(), + rent_exemption, + ) + .map_err(|_| CTokenError::InvalidAccountData)?; + + *lamports_budget = lamports_budget.saturating_sub(*transfer_amount); + + Ok(()) +} From 54a44611fcebed81e58d5064a6e852d88b61df3c Mon Sep 17 00:00:00 2001 From: ananas Date: Tue, 6 Jan 2026 21:49:15 +0000 Subject: [PATCH 07/18] fix: pinocchio token error conversion --- program-libs/ctoken-interface/src/error.rs | 4 ++ .../tests/ctoken/approve_revoke.rs | 4 +- .../tests/ctoken/burn.rs | 22 +++++----- .../tests/ctoken/transfer.rs | 14 +++---- programs/compressed-token/anchor/src/lib.rs | 32 +++++++++++++++ .../compressed_token/mint_action/accounts.rs | 2 +- .../actions/compress_and_close_cmint.rs | 4 +- .../mint_action/mint_input.rs | 4 +- .../mint_action/mint_output.rs | 22 +++++++--- .../mint_action/zero_copy_config.rs | 4 +- .../src/compressible/withdraw_funding_pool.rs | 4 +- .../program/src/ctoken/approve_revoke.rs | 18 +++++---- .../program/src/ctoken/burn.rs | 13 +++--- .../program/src/ctoken/freeze_thaw.rs | 6 +-- .../program/src/ctoken/mint_to.rs | 13 +++--- .../program/src/ctoken/transfer/checked.rs | 8 ++-- .../program/src/ctoken/transfer/default.rs | 5 ++- .../program/src/shared/compressible_top_up.rs | 4 +- .../src/shared/convert_program_error.rs | 40 +++++++++++++++++++ .../program/src/shared/mint_to_token_pool.rs | 4 +- .../program/src/shared/mod.rs | 2 +- 21 files changed, 159 insertions(+), 70 deletions(-) diff --git a/program-libs/ctoken-interface/src/error.rs b/program-libs/ctoken-interface/src/error.rs index e2a19d683e..801bedaaa0 100644 --- a/program-libs/ctoken-interface/src/error.rs +++ b/program-libs/ctoken-interface/src/error.rs @@ -188,6 +188,9 @@ pub enum CTokenError { "Decompress has withheld_transfer_fee but destination lacks TransferFeeAccount extension" )] DecompressWithheldFeeWithoutExtension, + + #[error("Missing required payer account")] + MissingPayer, } impl From for u32 { @@ -253,6 +256,7 @@ impl From for u32 { CTokenError::MintMismatch => 18058, CTokenError::DecompressDelegatedAmountWithoutDelegate => 18059, CTokenError::DecompressWithheldFeeWithoutExtension => 18060, + CTokenError::MissingPayer => 18061, CTokenError::HasherError(e) => u32::from(e), CTokenError::ZeroCopyError(e) => u32::from(e), CTokenError::CompressedAccountError(e) => u32::from(e), diff --git a/program-tests/compressed-token-test/tests/ctoken/approve_revoke.rs b/program-tests/compressed-token-test/tests/ctoken/approve_revoke.rs index c8021d4c5c..07c33537bf 100644 --- a/program-tests/compressed-token-test/tests/ctoken/approve_revoke.rs +++ b/program-tests/compressed-token-test/tests/ctoken/approve_revoke.rs @@ -83,7 +83,7 @@ async fn test_approve_fails() { 100, None, "non_existent_account", - 6000, // Pinocchio token program error - account doesn't exist + 6153, // NotRentExempt (SPL Token code 0 -> ErrorCode::NotRentExempt) ) .await; } @@ -253,7 +253,7 @@ async fn test_revoke_fails() { &owner, None, "non_existent_account", - 6000, // Pinocchio token program error - account doesn't exist + 6153, // NotRentExempt (SPL Token code 0 -> ErrorCode::NotRentExempt) ) .await; } diff --git a/program-tests/compressed-token-test/tests/ctoken/burn.rs b/program-tests/compressed-token-test/tests/ctoken/burn.rs index d27e1fe0e6..847365b6ec 100644 --- a/program-tests/compressed-token-test/tests/ctoken/burn.rs +++ b/program-tests/compressed-token-test/tests/ctoken/burn.rs @@ -103,12 +103,12 @@ async fn test_burn_success_cases() { // Burn Failure Cases // ============================================================================ -/// Error codes used in burn validation +/// Error codes used in burn validation (mapped to ErrorCode enum variants) mod error_codes { - /// Insufficient funds to complete the operation (SPL Token code 1) - pub const INSUFFICIENT_FUNDS: u32 = 1; - /// Authority doesn't match token account owner (SPL Token code 4) - pub const OWNER_MISMATCH: u32 = 4; + /// Insufficient funds to complete the operation (SplInsufficientFunds = 6154) + pub const INSUFFICIENT_FUNDS: u32 = 6154; + /// Authority doesn't match token account owner (OwnerMismatch = 6075) + pub const OWNER_MISMATCH: u32 = 6075; } #[tokio::test] @@ -142,8 +142,8 @@ async fn test_burn_fails() { ) .await; - // Non-existent CMint returns GenericError (code 0) - assert_rpc_error(result, 0, 0).unwrap(); + // Non-existent CMint returns NotRentExempt (SPL Token code 0 -> 6153) + assert_rpc_error(result, 0, 6153).unwrap(); println!("test_burn_fails: wrong mint passed"); } @@ -172,8 +172,8 @@ async fn test_burn_fails() { ) .await; - // Non-existent CToken account returns GenericError (code 0) - assert_rpc_error(result, 0, 0).unwrap(); + // Non-existent CToken account returns NotRentExempt (SPL Token code 0 -> 6153) + assert_rpc_error(result, 0, 6153).unwrap(); println!("test_burn_fails: non-existent account passed"); } @@ -399,8 +399,8 @@ async fn setup_burn_test() -> BurnTestContext { use light_ctoken_sdk::ctoken::BurnCTokenChecked; -/// MintDecimalsMismatch error code (SPL Token code 18) -const MINT_DECIMALS_MISMATCH: u32 = 18; +/// MintDecimalsMismatch error code (SplMintDecimalsMismatch = 6166) +const MINT_DECIMALS_MISMATCH: u32 = 6166; #[tokio::test] #[serial] diff --git a/program-tests/compressed-token-test/tests/ctoken/transfer.rs b/program-tests/compressed-token-test/tests/ctoken/transfer.rs index 998c825d93..bf58bb89ee 100644 --- a/program-tests/compressed-token-test/tests/ctoken/transfer.rs +++ b/program-tests/compressed-token-test/tests/ctoken/transfer.rs @@ -315,7 +315,7 @@ async fn test_ctoken_transfer_insufficient_balance() { let owner_keypair = context.owner_keypair.insecure_clone(); // Try to transfer more than the balance (1500 > 1000) - // Expected error: InsufficientFunds (error code 1) + // Expected error: SplInsufficientFunds (6154) transfer_and_assert_fails( &mut context, source, @@ -323,7 +323,7 @@ async fn test_ctoken_transfer_insufficient_balance() { 1500, &owner_keypair, "insufficient_balance_transfer", - 1, // InsufficientFunds + 6154, // SplInsufficientFunds ) .await; } @@ -393,7 +393,7 @@ async fn test_ctoken_transfer_wrong_authority() { let wrong_authority = Keypair::new(); // Try to transfer with wrong authority - // Expected error: OwnerMismatch (error code 4) + // Expected error: OwnerMismatch (6075) transfer_and_assert_fails( &mut context, source, @@ -401,7 +401,7 @@ async fn test_ctoken_transfer_wrong_authority() { 500, &wrong_authority, "wrong_authority_transfer", - 4, // OwnerMismatch + 6075, // OwnerMismatch ) .await; } @@ -425,7 +425,7 @@ async fn test_ctoken_transfer_mint_mismatch() { let owner_keypair = context.owner_keypair.insecure_clone(); // Try to transfer between accounts with different mints - // The SPL Token program returns error code 3 (MintMismatch) + // Expected error: SplMintMismatch (6155) transfer_and_assert_fails( &mut context, source, @@ -433,7 +433,7 @@ async fn test_ctoken_transfer_mint_mismatch() { 500, &owner_keypair, "mint_mismatch_transfer", - 3, // MintMismatch + 6155, // SplMintMismatch ) .await; } @@ -911,7 +911,7 @@ async fn test_ctoken_transfer_checked_insufficient_balance() { 9, &owner_keypair, "insufficient_balance_transfer_checked", - 1, // InsufficientFunds + 6154, // SplInsufficientFunds ) .await; } diff --git a/programs/compressed-token/anchor/src/lib.rs b/programs/compressed-token/anchor/src/lib.rs index 80dd79fca5..33346ab26b 100644 --- a/programs/compressed-token/anchor/src/lib.rs +++ b/programs/compressed-token/anchor/src/lib.rs @@ -548,6 +548,38 @@ pub enum ErrorCode { CompressionOnlyNotAllowed, // 6151 #[msg("Associated token accounts must have compression_only set")] AtaRequiresCompressionOnly, // 6152 + // ========================================================================= + // SPL Token compatible errors (mapped from pinocchio token processor) + // These mirror SPL Token error codes for consistent error reporting + // ========================================================================= + #[msg("Lamport balance below rent-exempt threshold")] + NotRentExempt, // 6153 (SPL Token code 0) + #[msg("Insufficient funds for the operation")] + InsufficientFunds, // 6154 (SPL Token code 1) + #[msg("Account not associated with this Mint")] + MintMismatch, // 6155 (SPL Token code 3) + #[msg("This token's supply is fixed and new tokens cannot be minted")] + FixedSupply, // 6156 (SPL Token code 5) + #[msg("Account already in use")] + AlreadyInUse, // 6157 (SPL Token code 6) + #[msg("Invalid number of provided signers")] + InvalidNumberOfProvidedSigners, // 6158 (SPL Token code 7) + #[msg("Invalid number of required signers")] + InvalidNumberOfRequiredSigners, // 6159 (SPL Token code 8) + #[msg("State is uninitialized")] + UninitializedState, // 6160 (SPL Token code 9) + #[msg("Instruction does not support native tokens")] + NativeNotSupported, // 6161 (SPL Token code 10) + #[msg("Invalid instruction")] + InvalidInstruction, // 6162 (SPL Token code 12) + #[msg("State is invalid for requested operation")] + InvalidState, // 6163 (SPL Token code 13) + #[msg("Operation overflowed")] + Overflow, // 6164 (SPL Token code 14) + #[msg("Account does not support specified authority type")] + AuthorityTypeNotSupported, // 6165 (SPL Token code 15) + #[msg("Mint decimals mismatch between the client and mint")] + MintDecimalsMismatch, // 6166 (SPL Token code 18) } /// Anchor error code offset - error codes start at 6000 diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs index 36ae0594e6..5e988aea56 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs @@ -364,7 +364,7 @@ impl AccountsConfig { /// This is the case when the mint is decompressed (or being decompressed) and not being closed. /// When true, compressed account uses zero sentinel values (discriminator=[0;8], data_hash=[0;32]). #[inline(always)] - pub fn cmint_is_source_of_truth(&self) -> bool { + pub fn cmint_is_decompressed(&self) -> bool { (self.has_decompress_mint_action || self.cmint_decompressed) && !self.has_compress_and_close_cmint_action } diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs index 9ea9f77194..46f6249db2 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs @@ -107,9 +107,7 @@ pub fn process_compress_and_close_cmint_action( unsafe { cmint.assign(&[0u8; 32]); } - cmint - .resize(0) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32 + 6000))?; + cmint.resize(0).map_err(convert_program_error)?; } // 8. Set cmint_decompressed = false compressed_mint.metadata.cmint_decompressed = false; diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs index a48de9e63b..80af25aac1 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs @@ -28,8 +28,8 @@ pub fn create_input_compressed_mint_account( accounts_config: &AccountsConfig, ) -> Result<(), ProgramError> { // When CMint was source of truth (input state BEFORE actions), use zero sentinel values - // Use cmint_decompressed directly, not cmint_is_source_of_truth(), because: - // - cmint_is_source_of_truth() tells us the OUTPUT state (after actions) + // Use cmint_decompressed directly, not cmint_is_decompressed(), because: + // - cmint_is_decompressed() tells us the OUTPUT state (after actions) // - cmint_decompressed tells us the INPUT state (before actions) // For CompressAndCloseCMint: input has zero values (was decompressed), output has real data let (discriminator, input_data_hash) = if accounts_config.cmint_decompressed { diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs index a1e227799b..1c417186e5 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs @@ -45,11 +45,13 @@ pub fn process_output_compressed_account<'a>( &mut compressed_mint, )?; // When decompressed (CMint is source of truth), use zero values - let cmint_is_source_of_truth = accounts_config.cmint_is_source_of_truth(); + // TODO: check whether I can just mutate is_decompressed instead of using has_compress_and_close_cmint_action + // TODO: double check that we cannot close and create in the same instruction + let cmint_is_decompressed = accounts_config.cmint_is_decompressed(); // Serialize state into CMint solana account // SKIP if CompressAndCloseCMint action is present (CMint is being closed) // SKIP if DecompressMint action is present (CMint is being closed) - if cmint_is_source_of_truth { + if cmint_is_decompressed { let cmint_account = validated_accounts .get_cmint() .ok_or(ErrorCode::CMintNotFound)?; @@ -138,9 +140,17 @@ pub fn process_output_compressed_account<'a>( .as_mut() .ok_or(ErrorCode::MintActionOutputSerializationFailed)?; - let (discriminator, data_hash) = if cmint_is_source_of_truth { - // Zero sentinel values indicate "data lives in CMint" - // Data buffer is empty (data_len=0), no serialization needed + let (discriminator, data_hash) = if cmint_is_decompressed { + if !compressed_account_data.data.is_empty() { + msg!( + "Data allocation for output mint account is wrong: {} (expected) != {} ", + 0, + compressed_account_data.data.len() + ); + return Err(ProgramError::InvalidAccountData); + } + // Zeroed discriminator and data hash preserve the address + // of a closed compressed account without any data. ([0u8; 8], [0u8; 32]) } else { // Serialize compressed mint for compressed account @@ -149,7 +159,7 @@ pub fn process_output_compressed_account<'a>( .map_err(|e| ProgramError::BorshIoError(e.to_string()))?; if data.len() != compressed_account_data.data.len() { msg!( - "Data allocation for output mint account is wrong: {} != {}", + "Data allocation for output mint account is wrong: {} (expected) != {}", data.len(), compressed_account_data.data.len() ); diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs b/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs index 5beba029ac..ecef2e7ebb 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs @@ -86,7 +86,7 @@ pub fn get_zero_copy_configs( return Err(ErrorCode::TooManyMintToRecipients.into()); } // CMint is source of truth when decompressed and not closing - let cmint_is_source_of_truth = accounts_config.cmint_is_source_of_truth(); + let cmint_is_decompressed = accounts_config.cmint_is_decompressed(); let input = CpiConfigInput { input_accounts: { @@ -101,7 +101,7 @@ pub fn get_zero_copy_configs( let mut outputs = ArrayVec::new(); // First output is always the mint account // When CMint is source of truth, use data_len=0 (zero discriminator/hash) - let mint_data_len = if cmint_is_source_of_truth { + let mint_data_len = if cmint_is_decompressed { 0 } else { mint_data_len(&output_mint_config) diff --git a/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs b/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs index 30c47200a3..0a54c94eb9 100644 --- a/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs +++ b/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs @@ -8,7 +8,7 @@ use pinocchio::{ use pinocchio_system::instructions::Transfer; use spl_pod::solana_msg::msg; -use crate::shared::parse_config_account; +use crate::shared::{convert_program_error, parse_config_account}; /// Accounts required for the withdraw funding pool instruction pub struct WithdrawFundingPoolAccounts<'a> { @@ -115,5 +115,5 @@ pub fn process_withdraw_funding_pool( transfer .invoke_signed(&[signer]) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32 + 6000)) + .map_err(convert_program_error) } diff --git a/programs/compressed-token/program/src/ctoken/approve_revoke.rs b/programs/compressed-token/program/src/ctoken/approve_revoke.rs index 1907ec697a..3fbd6201ce 100644 --- a/programs/compressed-token/program/src/ctoken/approve_revoke.rs +++ b/programs/compressed-token/program/src/ctoken/approve_revoke.rs @@ -7,8 +7,8 @@ use pinocchio_token_program::processor::{ }; use crate::shared::{ - compressible_top_up::process_compression_top_up, convert_program_error, - owner_validation::check_token_program_owner, transfer_lamports_via_cpi, + compressible_top_up::process_compression_top_up, convert_pinocchio_token_error, + convert_program_error, owner_validation::check_token_program_owner, transfer_lamports_via_cpi, }; /// Account indices for approve instruction @@ -39,7 +39,7 @@ pub fn process_ctoken_approve( let source = accounts .get(APPROVE_ACCOUNT_SOURCE) .ok_or(ProgramError::NotEnoughAccountKeys)?; - process_approve(accounts, &instruction_data[..8]).map_err(convert_program_error)?; + process_approve(accounts, &instruction_data[..8]).map_err(convert_pinocchio_token_error)?; // Hot path: 165-byte accounts have no extensions, just call pinocchio directly if source.data_len() == 165 { return Ok(()); @@ -77,7 +77,7 @@ pub fn process_ctoken_revoke( .get(REVOKE_ACCOUNT_SOURCE) .ok_or(ProgramError::NotEnoughAccountKeys)?; - process_revoke(accounts).map_err(convert_program_error)?; + process_revoke(accounts).map_err(convert_pinocchio_token_error)?; // Hot path: 165-byte accounts have no extensions if source.data_len() == 165 { @@ -86,7 +86,7 @@ pub fn process_ctoken_revoke( let payer = accounts .get(REVOKE_ACCOUNT_OWNER) - .ok_or(ProgramError::NotEnoughAccountKeys)?; + .ok_or(ProgramError::Custom(u32::from(CTokenError::MissingPayer)))?; // Parse max_top_up based on instruction data length (0 = no limit) let max_top_up = match instruction_data.len() { @@ -195,7 +195,7 @@ pub fn process_ctoken_approve_checked( if source.data_len() == 165 { check_token_program_owner(mint)?; return shared_process_approve(accounts, amount, Some(decimals)) - .map_err(convert_program_error); + .map_err(convert_pinocchio_token_error); } // Parse max_top_up from bytes 9-10 if present (0 = no limit) @@ -276,11 +276,13 @@ pub fn process_ctoken_approve_checked( } // Create 3-account slice [source, delegate, owner] - skip mint let approve_accounts = [*source, *delegate, *owner]; - shared_process_approve(&approve_accounts, amount, None).map_err(convert_program_error) + shared_process_approve(&approve_accounts, amount, None) + .map_err(convert_pinocchio_token_error) } else { // No cached decimals - validate via mint account check_token_program_owner(mint)?; // Use full 4-account layout [source, mint, delegate, owner] - shared_process_approve(accounts, amount, Some(decimals)).map_err(convert_program_error) + shared_process_approve(accounts, amount, Some(decimals)) + .map_err(convert_pinocchio_token_error) } } diff --git a/programs/compressed-token/program/src/ctoken/burn.rs b/programs/compressed-token/program/src/ctoken/burn.rs index 0b6a25b113..8817f18ffb 100644 --- a/programs/compressed-token/program/src/ctoken/burn.rs +++ b/programs/compressed-token/program/src/ctoken/burn.rs @@ -3,7 +3,9 @@ use light_program_profiler::profile; use pinocchio::account_info::AccountInfo; use pinocchio_token_program::processor::{burn::process_burn, burn_checked::process_burn_checked}; -use crate::shared::compressible_top_up::calculate_and_execute_compressible_top_ups; +use crate::shared::{ + compressible_top_up::calculate_and_execute_compressible_top_ups, convert_pinocchio_token_error, +}; /// Process ctoken burn instruction /// @@ -45,14 +47,13 @@ pub fn process_ctoken_burn( }; // Call pinocchio burn - handles balance/supply updates, authority check, frozen check - process_burn(accounts, &instruction_data[..8]) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32))?; + process_burn(accounts, &instruction_data[..8]).map_err(convert_pinocchio_token_error)?; // Calculate and execute top-ups for both CMint and CToken // burn account order: [ctoken, cmint, authority] - reverse of mint_to let ctoken = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; let cmint = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; - let payer = accounts.get(2).ok_or(ProgramError::NotEnoughAccountKeys)?; + let payer = accounts.get(2); calculate_and_execute_compressible_top_ups(cmint, ctoken, payer, max_top_up) } @@ -98,13 +99,13 @@ pub fn process_ctoken_burn_checked( // Call pinocchio burn_checked - validates decimals against CMint, handles balance/supply updates process_burn_checked(accounts, &instruction_data[..9]) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32))?; + .map_err(convert_pinocchio_token_error)?; // Calculate and execute top-ups for both CMint and CToken // burn account order: [ctoken, cmint, authority] - reverse of mint_to let ctoken = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; let cmint = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; - let payer = accounts.get(2).ok_or(ProgramError::NotEnoughAccountKeys)?; + let payer = accounts.get(2); calculate_and_execute_compressible_top_ups(cmint, ctoken, payer, max_top_up) } diff --git a/programs/compressed-token/program/src/ctoken/freeze_thaw.rs b/programs/compressed-token/program/src/ctoken/freeze_thaw.rs index e9fd15ee71..d3867bd0e8 100644 --- a/programs/compressed-token/program/src/ctoken/freeze_thaw.rs +++ b/programs/compressed-token/program/src/ctoken/freeze_thaw.rs @@ -4,7 +4,7 @@ use pinocchio_token_program::processor::{ freeze_account::process_freeze_account, thaw_account::process_thaw_account, }; -use crate::shared::owner_validation::check_token_program_owner; +use crate::shared::{convert_pinocchio_token_error, owner_validation::check_token_program_owner}; /// Process CToken freeze account instruction. /// Validates mint ownership before calling pinocchio-token-program. @@ -13,7 +13,7 @@ pub fn process_ctoken_freeze_account(accounts: &[AccountInfo]) -> Result<(), Pro // accounts[1] is the mint let mint_info = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; check_token_program_owner(mint_info)?; - process_freeze_account(accounts).map_err(|e| ProgramError::Custom(u64::from(e) as u32)) + process_freeze_account(accounts).map_err(convert_pinocchio_token_error) } /// Process CToken thaw account instruction. @@ -23,5 +23,5 @@ pub fn process_ctoken_thaw_account(accounts: &[AccountInfo]) -> Result<(), Progr // accounts[1] is the mint let mint_info = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; check_token_program_owner(mint_info)?; - process_thaw_account(accounts).map_err(|e| ProgramError::Custom(u64::from(e) as u32)) + process_thaw_account(accounts).map_err(convert_pinocchio_token_error) } diff --git a/programs/compressed-token/program/src/ctoken/mint_to.rs b/programs/compressed-token/program/src/ctoken/mint_to.rs index 215525987e..35b1a4aab4 100644 --- a/programs/compressed-token/program/src/ctoken/mint_to.rs +++ b/programs/compressed-token/program/src/ctoken/mint_to.rs @@ -5,7 +5,9 @@ use pinocchio_token_program::processor::{ mint_to::process_mint_to, mint_to_checked::process_mint_to_checked, }; -use crate::shared::compressible_top_up::calculate_and_execute_compressible_top_ups; +use crate::shared::{ + compressible_top_up::calculate_and_execute_compressible_top_ups, convert_pinocchio_token_error, +}; /// Process ctoken mint_to instruction /// @@ -47,14 +49,13 @@ pub fn process_ctoken_mint_to( }; // Call pinocchio mint_to - handles supply/balance updates, authority check, frozen check - process_mint_to(accounts, &instruction_data[..8]) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32))?; + process_mint_to(accounts, &instruction_data[..8]).map_err(convert_pinocchio_token_error)?; // Calculate and execute top-ups for both CMint and CToken // mint_to account order: [cmint, ctoken, authority] let cmint = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; let ctoken = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; - let payer = accounts.get(2).ok_or(ProgramError::NotEnoughAccountKeys)?; + let payer = accounts.get(2); calculate_and_execute_compressible_top_ups(cmint, ctoken, payer, max_top_up) } @@ -100,13 +101,13 @@ pub fn process_ctoken_mint_to_checked( // Call pinocchio mint_to_checked - validates decimals against CMint, handles supply/balance updates process_mint_to_checked(accounts, &instruction_data[..9]) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32))?; + .map_err(convert_pinocchio_token_error)?; // Calculate and execute top-ups for both CMint and CToken // mint_to account order: [cmint, ctoken, authority] let cmint = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; let ctoken = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; - let payer = accounts.get(2).ok_or(ProgramError::NotEnoughAccountKeys)?; + let payer = accounts.get(2); calculate_and_execute_compressible_top_ups(cmint, ctoken, payer, max_top_up) } diff --git a/programs/compressed-token/program/src/ctoken/transfer/checked.rs b/programs/compressed-token/program/src/ctoken/transfer/checked.rs index 1c850fbdb4..fa9578a304 100644 --- a/programs/compressed-token/program/src/ctoken/transfer/checked.rs +++ b/programs/compressed-token/program/src/ctoken/transfer/checked.rs @@ -7,7 +7,7 @@ use pinocchio_token_program::processor::{ }; use super::shared::{process_transfer_extensions_transfer_checked, TransferAccounts}; -use crate::shared::owner_validation::check_token_program_owner; +use crate::shared::{convert_pinocchio_token_error, owner_validation::check_token_program_owner}; /// Account indices for CToken transfer_checked instruction /// Note: Different from ctoken_transfer - mint is at index 1 const ACCOUNT_SOURCE: usize = 0; @@ -50,7 +50,7 @@ pub fn process_ctoken_transfer_checked( // Hot path: 165-byte accounts have no extensions, skip all extension processing if source.data_len() == 165 && destination.data_len() == 165 { return process_transfer_checked(accounts, &instruction_data[..9], false) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32)); + .map_err(convert_pinocchio_token_error); } let mint = accounts @@ -100,10 +100,10 @@ pub fn process_ctoken_transfer_checked( None, signer_is_validated, ) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32)) + .map_err(convert_pinocchio_token_error) } else { check_token_program_owner(mint)?; process_transfer(accounts, amount, Some(decimals), signer_is_validated) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32)) + .map_err(convert_pinocchio_token_error) } } diff --git a/programs/compressed-token/program/src/ctoken/transfer/default.rs b/programs/compressed-token/program/src/ctoken/transfer/default.rs index 02cb3bb94e..20e489df53 100644 --- a/programs/compressed-token/program/src/ctoken/transfer/default.rs +++ b/programs/compressed-token/program/src/ctoken/transfer/default.rs @@ -4,6 +4,7 @@ use pinocchio::account_info::AccountInfo; use pinocchio_token_program::processor::transfer::process_transfer; use super::shared::{process_transfer_extensions_transfer, TransferAccounts}; +use crate::shared::convert_pinocchio_token_error; /// Account indices for CToken transfer instruction const ACCOUNT_SOURCE: usize = 0; @@ -43,7 +44,7 @@ pub fn process_ctoken_transfer( .ok_or(ProgramError::NotEnoughAccountKeys)?; if source.data_len() == 165 && destination.data_len() == 165 { return process_transfer(accounts, &instruction_data[..8], false) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32)); + .map_err(convert_pinocchio_token_error); } // Parse max_top_up based on instruction data length @@ -62,7 +63,7 @@ pub fn process_ctoken_transfer( // Only pass the first 8 bytes (amount) to the SPL transfer processor process_transfer(accounts, &instruction_data[..8], signer_is_validated) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32)) + .map_err(convert_pinocchio_token_error) } fn process_extensions( diff --git a/programs/compressed-token/program/src/shared/compressible_top_up.rs b/programs/compressed-token/program/src/shared/compressible_top_up.rs index 1f4531aebd..3a1f732732 100644 --- a/programs/compressed-token/program/src/shared/compressible_top_up.rs +++ b/programs/compressed-token/program/src/shared/compressible_top_up.rs @@ -27,7 +27,7 @@ use super::{ pub fn calculate_and_execute_compressible_top_ups<'a>( cmint: &'a AccountInfo, ctoken: &'a AccountInfo, - payer: &'a AccountInfo, + payer: Option<&'a AccountInfo>, max_top_up: u16, ) -> Result<(), ProgramError> { let mut transfers = [ @@ -93,7 +93,7 @@ pub fn calculate_and_execute_compressible_top_ups<'a>( if max_top_up != 0 && lamports_budget == 0 { return Err(CTokenError::MaxTopUpExceeded.into()); } - + let payer = payer.ok_or(CTokenError::MissingPayer)?; multi_transfer_lamports(payer, &transfers).map_err(convert_program_error)?; Ok(()) } diff --git a/programs/compressed-token/program/src/shared/convert_program_error.rs b/programs/compressed-token/program/src/shared/convert_program_error.rs index e04888d0c8..764d07395f 100644 --- a/programs/compressed-token/program/src/shared/convert_program_error.rs +++ b/programs/compressed-token/program/src/shared/convert_program_error.rs @@ -1,5 +1,45 @@ +use anchor_compressed_token::ErrorCode; + +/// Convert generic pinocchio errors to anchor ProgramError with +6000 offset. +/// Use this for system program operations, data access, and non-token operations. pub fn convert_program_error( pinocchio_program_error: pinocchio::program_error::ProgramError, ) -> anchor_lang::prelude::ProgramError { anchor_lang::prelude::ProgramError::Custom(u64::from(pinocchio_program_error) as u32 + 6000) } + +/// Convert pinocchio token processor errors to our custom ErrorCode. +/// Maps SPL Token error codes (0-18) to our enum variants for consistent error reporting. +/// +/// IMPORTANT: Only use this for pinocchio_token_program processor calls. +/// For system program and other operations, use `convert_program_error` instead. +pub fn convert_pinocchio_token_error( + pinocchio_error: pinocchio::program_error::ProgramError, +) -> anchor_lang::prelude::ProgramError { + let code = u64::from(pinocchio_error) as u32; + + let error_code = match code { + 0 => ErrorCode::NotRentExempt, + 1 => ErrorCode::InsufficientFunds, + 2 => ErrorCode::InvalidMint, + 3 => ErrorCode::MintMismatch, + 4 => ErrorCode::OwnerMismatch, + 5 => ErrorCode::FixedSupply, + 6 => ErrorCode::AlreadyInUse, + 7 => ErrorCode::InvalidNumberOfProvidedSigners, + 8 => ErrorCode::InvalidNumberOfRequiredSigners, + 9 => ErrorCode::UninitializedState, + 10 => ErrorCode::NativeNotSupported, + 11 => ErrorCode::NonNativeHasBalance, + 12 => ErrorCode::InvalidInstruction, + 13 => ErrorCode::InvalidState, + 14 => ErrorCode::Overflow, + 15 => ErrorCode::AuthorityTypeNotSupported, + 16 => ErrorCode::MintHasNoFreezeAuthority, + 17 => ErrorCode::AccountFrozen, + 18 => ErrorCode::MintDecimalsMismatch, + // Pass through unknown/higher codes with standard +6900 offset + _ => return anchor_lang::prelude::ProgramError::Custom(code + 6900), + }; + error_code.into() +} diff --git a/programs/compressed-token/program/src/shared/mint_to_token_pool.rs b/programs/compressed-token/program/src/shared/mint_to_token_pool.rs index 82a7b666bf..42c2b80fd8 100644 --- a/programs/compressed-token/program/src/shared/mint_to_token_pool.rs +++ b/programs/compressed-token/program/src/shared/mint_to_token_pool.rs @@ -7,7 +7,7 @@ use pinocchio::{ program::invoke_signed, }; -use crate::LIGHT_CPI_SIGNER; +use crate::{shared::convert_program_error, LIGHT_CPI_SIGNER}; /// Mint tokens to the token pool using SPL token mint_to instruction. /// This function is shared between create_spl_mint and mint_to_compressed processors @@ -55,5 +55,5 @@ pub fn mint_to_token_pool( &[mint_account, token_pool_account, cpi_authority_pda], &[signer], ) - .map_err(|e| ProgramError::Custom(u64::from(e) as u32 + 6000)) + .map_err(convert_program_error) } diff --git a/programs/compressed-token/program/src/shared/mod.rs b/programs/compressed-token/program/src/shared/mod.rs index 3f52465d75..ee52d7043f 100644 --- a/programs/compressed-token/program/src/shared/mod.rs +++ b/programs/compressed-token/program/src/shared/mod.rs @@ -14,7 +14,7 @@ pub mod transfer_lamports; pub mod validate_ata_derivation; pub use config_account::{next_config_account, parse_config_account}; -pub use convert_program_error::convert_program_error; +pub use convert_program_error::{convert_pinocchio_token_error, convert_program_error}; pub use create_pda_account::{create_pda_account, verify_pda}; pub use initialize_ctoken_account::create_compressible_account; pub use light_account_checks::AccountIterator; From c8890691b5d3ac23900ba6a81d6facc1d73e79ee Mon Sep 17 00:00:00 2001 From: ananas Date: Tue, 6 Jan 2026 22:09:31 +0000 Subject: [PATCH 08/18] refactor: simplify ctoken account access with direct indexing - Replace redundant .get().ok_or() with direct indexing after length validation - Add SAFETY comments documenting the length invariants - Make payer optional - only required when top-up transfer is needed - Remove redundant process_extensions function in transfer/default.rs - Use CTokenError::MissingPayer consistently when payer is required --- .../program/src/ctoken/approve_revoke.rs | 28 +++++-------- .../program/src/ctoken/burn.rs | 10 +++-- .../program/src/ctoken/mint_to.rs | 10 +++-- .../program/src/ctoken/transfer/checked.rs | 18 +++------ .../program/src/ctoken/transfer/default.rs | 40 +++++-------------- 5 files changed, 36 insertions(+), 70 deletions(-) diff --git a/programs/compressed-token/program/src/ctoken/approve_revoke.rs b/programs/compressed-token/program/src/ctoken/approve_revoke.rs index 3fbd6201ce..e13b56e55e 100644 --- a/programs/compressed-token/program/src/ctoken/approve_revoke.rs +++ b/programs/compressed-token/program/src/ctoken/approve_revoke.rs @@ -45,9 +45,7 @@ pub fn process_ctoken_approve( return Ok(()); } - let payer = accounts - .get(APPROVE_ACCOUNT_OWNER) - .ok_or(ProgramError::NotEnoughAccountKeys)?; + let payer = accounts.get(APPROVE_ACCOUNT_OWNER); // Parse max_top_up based on instruction data length (0 = no limit) let max_top_up = match instruction_data.len() { @@ -84,9 +82,7 @@ pub fn process_ctoken_revoke( return Ok(()); } - let payer = accounts - .get(REVOKE_ACCOUNT_OWNER) - .ok_or(ProgramError::Custom(u32::from(CTokenError::MissingPayer)))?; + let payer = accounts.get(REVOKE_ACCOUNT_OWNER); // Parse max_top_up based on instruction data length (0 = no limit) let max_top_up = match instruction_data.len() { @@ -109,7 +105,7 @@ pub fn process_ctoken_revoke( #[inline(always)] fn process_compressible_top_up( account: &AccountInfo, - payer: &AccountInfo, + payer: Option<&AccountInfo>, max_top_up: u16, ) -> Result<(), ProgramError> { // Borrow account data to get extensions @@ -143,6 +139,7 @@ fn process_compressible_top_up( drop(account_data); if transfer_amount > 0 { + let payer = payer.ok_or(CTokenError::MissingPayer)?; transfer_lamports_via_cpi(transfer_amount, payer, account) .map_err(convert_program_error)?; } @@ -183,12 +180,9 @@ pub fn process_ctoken_approve_checked( let (amount, decimals) = unpack_amount_and_decimals(instruction_data).map_err(|e| ProgramError::Custom(e as u32))?; - let source = accounts - .get(APPROVE_CHECKED_ACCOUNT_SOURCE) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let mint = accounts - .get(APPROVE_CHECKED_ACCOUNT_MINT) - .ok_or(ProgramError::NotEnoughAccountKeys)?; + // SAFETY: accounts.len() >= 4 validated at function entry + let source = &accounts[APPROVE_CHECKED_ACCOUNT_SOURCE]; + let mint = &accounts[APPROVE_CHECKED_ACCOUNT_MINT]; // Hot path: 165-byte accounts have no extensions (no cached decimals, no top-up) // Validate via mint and use full 4-account layout @@ -209,12 +203,8 @@ pub fn process_ctoken_approve_checked( _ => return Err(ProgramError::InvalidInstructionData), }; - let delegate = accounts - .get(APPROVE_CHECKED_ACCOUNT_DELEGATE) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let owner = accounts - .get(APPROVE_CHECKED_ACCOUNT_OWNER) - .ok_or(ProgramError::NotEnoughAccountKeys)?; + let delegate = &accounts[APPROVE_CHECKED_ACCOUNT_DELEGATE]; + let owner = &accounts[APPROVE_CHECKED_ACCOUNT_OWNER]; // Borrow source account to check for cached decimals and handle top-up let cached_decimals = { diff --git a/programs/compressed-token/program/src/ctoken/burn.rs b/programs/compressed-token/program/src/ctoken/burn.rs index 8817f18ffb..aedd2b8f15 100644 --- a/programs/compressed-token/program/src/ctoken/burn.rs +++ b/programs/compressed-token/program/src/ctoken/burn.rs @@ -51,8 +51,9 @@ pub fn process_ctoken_burn( // Calculate and execute top-ups for both CMint and CToken // burn account order: [ctoken, cmint, authority] - reverse of mint_to - let ctoken = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; - let cmint = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; + // SAFETY: accounts.len() >= 3 validated at function entry + let ctoken = &accounts[0]; + let cmint = &accounts[1]; let payer = accounts.get(2); calculate_and_execute_compressible_top_ups(cmint, ctoken, payer, max_top_up) @@ -103,8 +104,9 @@ pub fn process_ctoken_burn_checked( // Calculate and execute top-ups for both CMint and CToken // burn account order: [ctoken, cmint, authority] - reverse of mint_to - let ctoken = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; - let cmint = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; + // SAFETY: accounts.len() >= 3 validated at function entry + let ctoken = &accounts[0]; + let cmint = &accounts[1]; let payer = accounts.get(2); calculate_and_execute_compressible_top_ups(cmint, ctoken, payer, max_top_up) diff --git a/programs/compressed-token/program/src/ctoken/mint_to.rs b/programs/compressed-token/program/src/ctoken/mint_to.rs index 35b1a4aab4..66c9f2bee5 100644 --- a/programs/compressed-token/program/src/ctoken/mint_to.rs +++ b/programs/compressed-token/program/src/ctoken/mint_to.rs @@ -53,8 +53,9 @@ pub fn process_ctoken_mint_to( // Calculate and execute top-ups for both CMint and CToken // mint_to account order: [cmint, ctoken, authority] - let cmint = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; - let ctoken = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; + // SAFETY: accounts.len() >= 3 validated at function entry + let cmint = &accounts[0]; + let ctoken = &accounts[1]; let payer = accounts.get(2); calculate_and_execute_compressible_top_ups(cmint, ctoken, payer, max_top_up) @@ -105,8 +106,9 @@ pub fn process_ctoken_mint_to_checked( // Calculate and execute top-ups for both CMint and CToken // mint_to account order: [cmint, ctoken, authority] - let cmint = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; - let ctoken = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?; + // SAFETY: accounts.len() >= 3 validated at function entry + let cmint = &accounts[0]; + let ctoken = &accounts[1]; let payer = accounts.get(2); calculate_and_execute_compressible_top_ups(cmint, ctoken, payer, max_top_up) diff --git a/programs/compressed-token/program/src/ctoken/transfer/checked.rs b/programs/compressed-token/program/src/ctoken/transfer/checked.rs index fa9578a304..27efa78cd1 100644 --- a/programs/compressed-token/program/src/ctoken/transfer/checked.rs +++ b/programs/compressed-token/program/src/ctoken/transfer/checked.rs @@ -39,13 +39,9 @@ pub fn process_ctoken_transfer_checked( return Err(ProgramError::InvalidInstructionData); } - // Get account references - let source = accounts - .get(ACCOUNT_SOURCE) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let destination = accounts - .get(ACCOUNT_DESTINATION) - .ok_or(ProgramError::NotEnoughAccountKeys)?; + // SAFETY: accounts.len() >= 4 validated at function entry + let source = &accounts[ACCOUNT_SOURCE]; + let destination = &accounts[ACCOUNT_DESTINATION]; // Hot path: 165-byte accounts have no extensions, skip all extension processing if source.data_len() == 165 && destination.data_len() == 165 { @@ -53,12 +49,8 @@ pub fn process_ctoken_transfer_checked( .map_err(convert_pinocchio_token_error); } - let mint = accounts - .get(ACCOUNT_MINT) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let authority = accounts - .get(ACCOUNT_AUTHORITY) - .ok_or(ProgramError::NotEnoughAccountKeys)?; + let mint = &accounts[ACCOUNT_MINT]; + let authority = &accounts[ACCOUNT_AUTHORITY]; // Parse max_top_up based on instruction data length // 0 means no limit diff --git a/programs/compressed-token/program/src/ctoken/transfer/default.rs b/programs/compressed-token/program/src/ctoken/transfer/default.rs index 20e489df53..5d63f3accf 100644 --- a/programs/compressed-token/program/src/ctoken/transfer/default.rs +++ b/programs/compressed-token/program/src/ctoken/transfer/default.rs @@ -36,12 +36,9 @@ pub fn process_ctoken_transfer( } // Hot path: 165-byte accounts have no extensions, skip all extension processing - let source = accounts - .get(ACCOUNT_SOURCE) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let destination = accounts - .get(ACCOUNT_DESTINATION) - .ok_or(ProgramError::NotEnoughAccountKeys)?; + // SAFETY: accounts.len() >= 3 validated at function entry + let source = &accounts[ACCOUNT_SOURCE]; + let destination = &accounts[ACCOUNT_DESTINATION]; if source.data_len() == 165 && destination.data_len() == 165 { return process_transfer(accounts, &instruction_data[..8], false) .map_err(convert_pinocchio_token_error); @@ -59,36 +56,19 @@ pub fn process_ctoken_transfer( _ => return Err(ProgramError::InvalidInstructionData), }; - let signer_is_validated = process_extensions(accounts, max_top_up)?; + let authority = &accounts[ACCOUNT_AUTHORITY]; - // Only pass the first 8 bytes (amount) to the SPL transfer processor - process_transfer(accounts, &instruction_data[..8], signer_is_validated) - .map_err(convert_pinocchio_token_error) -} - -fn process_extensions( - accounts: &[pinocchio::account_info::AccountInfo], - max_top_up: u16, -) -> Result { - let source = accounts - .get(ACCOUNT_SOURCE) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let destination = accounts - .get(ACCOUNT_DESTINATION) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let authority = accounts - .get(ACCOUNT_AUTHORITY) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - - // Ignore decimals - only used for transfer_checked - let (signer_is_validated, _decimals) = process_transfer_extensions_transfer( + let (signer_is_validated, _) = process_transfer_extensions_transfer( TransferAccounts { source, destination, authority, - mint: None, + mint: None, // No mint in transfer instruction }, max_top_up, )?; - Ok(signer_is_validated) + + // Only pass the first 8 bytes (amount) to the SPL transfer processor + process_transfer(accounts, &instruction_data[..8], signer_is_validated) + .map_err(convert_pinocchio_token_error) } From bf219fb5b0090465aebc7473d86d2483b5bbfcc6 Mon Sep 17 00:00:00 2001 From: ananas Date: Tue, 6 Jan 2026 22:45:27 +0000 Subject: [PATCH 09/18] fix: error conversions --- Cargo.lock | 4 +- Cargo.toml | 2 +- .../tests/mint/ctoken_mint_to.rs | 4 +- programs/compressed-token/program/CLAUDE.md | 49 +++++++++++++++++++ .../compressed_token/transfer2/processor.rs | 19 +++++-- .../program/src/ctoken/approve_revoke.rs | 6 +-- .../program/src/ctoken/transfer/checked.rs | 5 +- .../src/shared/convert_program_error.rs | 13 ++++- .../program/src/shared/mod.rs | 2 +- 9 files changed, 86 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb42bc283c..71fde9c988 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5072,7 +5072,7 @@ dependencies = [ [[package]] name = "pinocchio-token-interface" version = "0.0.0" -source = "git+https://github.com/Lightprotocol/token?rev=1bf7a9e525e753c3eb7bbf9971a26efbc23e5c73#1bf7a9e525e753c3eb7bbf9971a26efbc23e5c73" +source = "git+https://github.com/Lightprotocol/token?rev=0c55d18#0c55d185aaede4e83039ebfeb7a2caa000253450" dependencies = [ "pinocchio", "pinocchio-pubkey", @@ -5081,7 +5081,7 @@ dependencies = [ [[package]] name = "pinocchio-token-program" version = "0.1.0" -source = "git+https://github.com/Lightprotocol/token?rev=1bf7a9e525e753c3eb7bbf9971a26efbc23e5c73#1bf7a9e525e753c3eb7bbf9971a26efbc23e5c73" +source = "git+https://github.com/Lightprotocol/token?rev=0c55d18#0c55d185aaede4e83039ebfeb7a2caa000253450" dependencies = [ "pinocchio", "pinocchio-log", diff --git a/Cargo.toml b/Cargo.toml index 26d1df64dc..06653ca36b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -232,7 +232,7 @@ groth16-solana = { version = "0.2.0" } bytemuck = { version = "1.19.0" } arrayvec = "0.7" tinyvec = "1.10.0" -pinocchio-token-program = { git= "https://github.com/Lightprotocol/token", rev="1bf7a9e525e753c3eb7bbf9971a26efbc23e5c73" } +pinocchio-token-program = { git= "https://github.com/Lightprotocol/token", rev="0c55d18" } # Math and crypto num-bigint = "0.4.6" tabled = "0.20" diff --git a/program-tests/compressed-token-test/tests/mint/ctoken_mint_to.rs b/program-tests/compressed-token-test/tests/mint/ctoken_mint_to.rs index e60c4bc481..e0b9882f2f 100644 --- a/program-tests/compressed-token-test/tests/mint/ctoken_mint_to.rs +++ b/program-tests/compressed-token-test/tests/mint/ctoken_mint_to.rs @@ -223,8 +223,8 @@ async fn test_ctoken_mint_to_checked_wrong_decimals() { ) .await; - // Should fail with MintDecimalsMismatch (error code 18 in pinocchio) + // Should fail with MintDecimalsMismatch (error code 18 in pinocchio mapped to 6166) assert!(result.is_err(), "Mint with wrong decimals should fail"); - light_program_test::utils::assert::assert_rpc_error(result, 0, 18).unwrap(); + light_program_test::utils::assert::assert_rpc_error(result, 0, 6166).unwrap(); println!("test_ctoken_mint_to_checked_wrong_decimals: passed"); } diff --git a/programs/compressed-token/program/CLAUDE.md b/programs/compressed-token/program/CLAUDE.md index 34b21a916c..9502adc282 100644 --- a/programs/compressed-token/program/CLAUDE.md +++ b/programs/compressed-token/program/CLAUDE.md @@ -206,6 +206,55 @@ Custom error codes are defined in **`programs/compressed-token/anchor/src/lib.rs - Errors are returned as `ProgramError::Custom(error_code as u32)` on-chain - CToken-specific errors are also defined in **`program-libs/ctoken-interface/src/error.rs`** (`CTokenError` enum) +### Error Conversion Functions (`shared/convert_program_error.rs`) + +Two functions exist for converting pinocchio errors to anchor ProgramError: + +| Function | Use Case | Error Mapping | +|----------|----------|---------------| +| `convert_pinocchio_token_error` | SPL Token operations via pinocchio_token_program processors | Maps SPL Token error codes (0-18) to named ErrorCode variants | +| `convert_token_error` | Functions returning TokenError directly (e.g., unpack_amount_and_decimals) | Maps SPL Token error codes (0-18) to named ErrorCode variants | +| `convert_program_error` | System program, data access, lamport transfers | Adds +6000 offset to raw error code | + +**When to use each:** + +```rust +// SPL Token operations - use convert_pinocchio_token_error +process_transfer(accounts, data).map_err(convert_pinocchio_token_error)?; +process_burn(accounts, data).map_err(convert_pinocchio_token_error)?; +process_mint_to(accounts, data).map_err(convert_pinocchio_token_error)?; + +// System/internal operations - use convert_program_error +transfer_lamports_via_cpi(...).map_err(convert_program_error)?; +account.try_borrow_mut_data().map_err(convert_program_error)?; + +// ErrorCode variants - use ProgramError::from directly +sum_check_multi_mint(...).map_err(ProgramError::from)?; +validate_mint_uniqueness(...).map_err(ProgramError::from)?; +``` + +**SPL Token Error Code Mapping:** +| SPL Code | ErrorCode Variant | Description | +|----------|-------------------|-------------| +| 0 | NotRentExempt | Lamport balance below rent-exempt threshold | +| 1 | InsufficientFunds | Insufficient funds for the operation | +| 2 | InvalidMint | Invalid mint account | +| 3 | MintMismatch | Account not associated with this Mint | +| 4 | OwnerMismatch | Owner does not match | +| 5 | FixedSupply | Token supply is fixed | +| 6 | AlreadyInUse | Account already in use | +| 7-8 | InvalidNumberOf*Signers | Signer count mismatch | +| 9 | UninitializedState | State is uninitialized | +| 10 | NativeNotSupported | Native tokens not supported | +| 11 | NonNativeHasBalance | Non-native account has balance | +| 12 | InvalidInstruction | Invalid instruction | +| 13 | InvalidState | State is invalid | +| 14 | Overflow | Operation overflowed | +| 15 | AuthorityTypeNotSupported | Authority type not supported | +| 16 | MintHasNoFreezeAuthority | Mint cannot freeze | +| 17 | AccountFrozen | Account is frozen | +| 18 | MintDecimalsMismatch | Decimals mismatch | + ## SDKs (`sdk-libs/`) - **`ctoken-sdk/`** - SDK for programs to interact with compressed tokens (CPIs, instruction builders) - **`token-client/`** - Client SDK for Rust applications (test helpers, transaction builders) diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/processor.rs b/programs/compressed-token/program/src/compressed_token/transfer2/processor.rs index 20fd0c0875..8e7597b029 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/processor.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/processor.rs @@ -141,9 +141,18 @@ pub fn validate_instruction_data( if !allowed { return Err(CTokenError::CompressedTokenAccountTlvUnimplemented); } + // All out_token_data must be version 3 if tlv is present. + let allowed = inputs.out_token_data.iter().all(|c| c.version == 3); + if !allowed { + return Err(CTokenError::CompressedTokenAccountTlvUnimplemented); + } // Output count must match compressions count (no extra outputs) - let compressions_len = inputs.compressions.as_ref().map(|c| c.len()).unwrap_or(0); + let compressions_len = inputs + .compressions + .as_ref() + .map(|c| c.len()) + .ok_or(CTokenError::OutTlvOutputCountMismatch)?; if inputs.out_token_data.len() != compressions_len { msg!("out_tlv requires out_token_data.len() == compressions.len()"); return Err(CTokenError::OutTlvOutputCountMismatch); @@ -186,11 +195,11 @@ fn process_no_system_program_cpi<'a>( let mint_map: ArrayMap = sum_check_multi_mint(&[], &[], Some(compressions.as_slice())) - .map_err(|e| ProgramError::Custom(e as u32 + 6000))?; + .map_err(ProgramError::from)?; // Validate mint uniqueness validate_mint_uniqueness(&mint_map, &validated_accounts.packed_accounts) - .map_err(|e| ProgramError::Custom(e as u32 + 6000))?; + .map_err(ProgramError::from)?; // This is the compression-only hot path (no compressed inputs/outputs). // Extension checks are skipped because balance must be restored immediately @@ -263,11 +272,11 @@ fn process_with_system_program_cpi<'a>( &inputs.out_token_data, inputs.compressions.as_deref(), ) - .map_err(|e| ProgramError::Custom(e as u32 + 6000))?; + .map_err(ProgramError::from)?; // Validate mint uniqueness validate_mint_uniqueness(&mint_map, &validated_accounts.packed_accounts) - .map_err(|e| ProgramError::Custom(e as u32 + 6000))?; + .map_err(ProgramError::from)?; if let Some(system_accounts) = validated_accounts.system.as_ref() { // Process token compressions/decompressions/close_and_compress diff --git a/programs/compressed-token/program/src/ctoken/approve_revoke.rs b/programs/compressed-token/program/src/ctoken/approve_revoke.rs index e13b56e55e..af859dea89 100644 --- a/programs/compressed-token/program/src/ctoken/approve_revoke.rs +++ b/programs/compressed-token/program/src/ctoken/approve_revoke.rs @@ -8,7 +8,8 @@ use pinocchio_token_program::processor::{ use crate::shared::{ compressible_top_up::process_compression_top_up, convert_pinocchio_token_error, - convert_program_error, owner_validation::check_token_program_owner, transfer_lamports_via_cpi, + convert_program_error, convert_token_error, owner_validation::check_token_program_owner, + transfer_lamports_via_cpi, }; /// Account indices for approve instruction @@ -177,8 +178,7 @@ pub fn process_ctoken_approve_checked( } // Parse amount and decimals from instruction data - let (amount, decimals) = - unpack_amount_and_decimals(instruction_data).map_err(|e| ProgramError::Custom(e as u32))?; + let (amount, decimals) = unpack_amount_and_decimals(instruction_data).map_err(convert_token_error)?; // SAFETY: accounts.len() >= 4 validated at function entry let source = &accounts[APPROVE_CHECKED_ACCOUNT_SOURCE]; diff --git a/programs/compressed-token/program/src/ctoken/transfer/checked.rs b/programs/compressed-token/program/src/ctoken/transfer/checked.rs index 27efa78cd1..20cf3863b3 100644 --- a/programs/compressed-token/program/src/ctoken/transfer/checked.rs +++ b/programs/compressed-token/program/src/ctoken/transfer/checked.rs @@ -7,7 +7,7 @@ use pinocchio_token_program::processor::{ }; use super::shared::{process_transfer_extensions_transfer_checked, TransferAccounts}; -use crate::shared::{convert_pinocchio_token_error, owner_validation::check_token_program_owner}; +use crate::shared::{convert_pinocchio_token_error, convert_token_error, owner_validation::check_token_program_owner}; /// Account indices for CToken transfer_checked instruction /// Note: Different from ctoken_transfer - mint is at index 1 const ACCOUNT_SOURCE: usize = 0; @@ -75,8 +75,7 @@ pub fn process_ctoken_transfer_checked( )?; // Pass the first 9 bytes (amount + decimals) to the SPL transfer_checked processor - let (amount, decimals) = - unpack_amount_and_decimals(instruction_data).map_err(|e| ProgramError::Custom(e as u32))?; + let (amount, decimals) = unpack_amount_and_decimals(instruction_data).map_err(convert_token_error)?; if let Some(extension_decimals) = extension_decimals { if extension_decimals != decimals { diff --git a/programs/compressed-token/program/src/shared/convert_program_error.rs b/programs/compressed-token/program/src/shared/convert_program_error.rs index 764d07395f..b8c114567e 100644 --- a/programs/compressed-token/program/src/shared/convert_program_error.rs +++ b/programs/compressed-token/program/src/shared/convert_program_error.rs @@ -1,4 +1,5 @@ use anchor_compressed_token::ErrorCode; +use pinocchio_token_program::error::TokenError; /// Convert generic pinocchio errors to anchor ProgramError with +6000 offset. /// Use this for system program operations, data access, and non-token operations. @@ -8,6 +9,12 @@ pub fn convert_program_error( anchor_lang::prelude::ProgramError::Custom(u64::from(pinocchio_program_error) as u32 + 6000) } +/// Convert TokenError directly to anchor ProgramError. +/// Use for functions returning TokenError (e.g., unpack_amount_and_decimals). +pub fn convert_token_error(e: TokenError) -> anchor_lang::prelude::ProgramError { + convert_spl_token_error_code(e as u32) +} + /// Convert pinocchio token processor errors to our custom ErrorCode. /// Maps SPL Token error codes (0-18) to our enum variants for consistent error reporting. /// @@ -16,8 +23,12 @@ pub fn convert_program_error( pub fn convert_pinocchio_token_error( pinocchio_error: pinocchio::program_error::ProgramError, ) -> anchor_lang::prelude::ProgramError { - let code = u64::from(pinocchio_error) as u32; + convert_spl_token_error_code(u64::from(pinocchio_error) as u32) +} +/// Internal: Map SPL Token error code (0-18) to ErrorCode. +#[inline(never)] +fn convert_spl_token_error_code(code: u32) -> anchor_lang::prelude::ProgramError { let error_code = match code { 0 => ErrorCode::NotRentExempt, 1 => ErrorCode::InsufficientFunds, diff --git a/programs/compressed-token/program/src/shared/mod.rs b/programs/compressed-token/program/src/shared/mod.rs index ee52d7043f..c372db8f9f 100644 --- a/programs/compressed-token/program/src/shared/mod.rs +++ b/programs/compressed-token/program/src/shared/mod.rs @@ -14,7 +14,7 @@ pub mod transfer_lamports; pub mod validate_ata_derivation; pub use config_account::{next_config_account, parse_config_account}; -pub use convert_program_error::{convert_pinocchio_token_error, convert_program_error}; +pub use convert_program_error::{convert_pinocchio_token_error, convert_program_error, convert_token_error}; pub use create_pda_account::{create_pda_account, verify_pda}; pub use initialize_ctoken_account::create_compressible_account; pub use light_account_checks::AccountIterator; From c72b0a95850fd9093bc4b9d74fdd184c1fc6ffa5 Mon Sep 17 00:00:00 2001 From: ananas Date: Tue, 6 Jan 2026 22:49:08 +0000 Subject: [PATCH 10/18] fix stackframe issues --- .../program/src/ctoken/transfer/default.rs | 18 +++++++++++++----- .../src/shared/convert_program_error.rs | 1 - 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/programs/compressed-token/program/src/ctoken/transfer/default.rs b/programs/compressed-token/program/src/ctoken/transfer/default.rs index 5d63f3accf..09bb0a8954 100644 --- a/programs/compressed-token/program/src/ctoken/transfer/default.rs +++ b/programs/compressed-token/program/src/ctoken/transfer/default.rs @@ -56,6 +56,17 @@ pub fn process_ctoken_transfer( _ => return Err(ProgramError::InvalidInstructionData), }; + let signer_is_validated = process_extensions(accounts, max_top_up)?; + + // Only pass the first 8 bytes (amount) to the SPL transfer processor + process_transfer(accounts, &instruction_data[..8], signer_is_validated) + .map_err(convert_pinocchio_token_error) +} + +fn process_extensions(accounts: &[AccountInfo], max_top_up: u16) -> Result { + // SAFETY: accounts.len() >= 3 validated in caller + let source = &accounts[ACCOUNT_SOURCE]; + let destination = &accounts[ACCOUNT_DESTINATION]; let authority = &accounts[ACCOUNT_AUTHORITY]; let (signer_is_validated, _) = process_transfer_extensions_transfer( @@ -63,12 +74,9 @@ pub fn process_ctoken_transfer( source, destination, authority, - mint: None, // No mint in transfer instruction + mint: None, }, max_top_up, )?; - - // Only pass the first 8 bytes (amount) to the SPL transfer processor - process_transfer(accounts, &instruction_data[..8], signer_is_validated) - .map_err(convert_pinocchio_token_error) + Ok(signer_is_validated) } diff --git a/programs/compressed-token/program/src/shared/convert_program_error.rs b/programs/compressed-token/program/src/shared/convert_program_error.rs index b8c114567e..a440c81496 100644 --- a/programs/compressed-token/program/src/shared/convert_program_error.rs +++ b/programs/compressed-token/program/src/shared/convert_program_error.rs @@ -27,7 +27,6 @@ pub fn convert_pinocchio_token_error( } /// Internal: Map SPL Token error code (0-18) to ErrorCode. -#[inline(never)] fn convert_spl_token_error_code(code: u32) -> anchor_lang::prelude::ProgramError { let error_code = match code { 0 => ErrorCode::NotRentExempt, From 13965d546df45e838f1854b4d8337734314b8f44 Mon Sep 17 00:00:00 2001 From: ananas Date: Wed, 7 Jan 2026 00:09:46 +0000 Subject: [PATCH 11/18] refactor and test check extensions transfer2 --- .../transfer2/check_extensions.rs | 29 +- .../transfer2/compression/mod.rs | 5 +- .../program/src/shared/owner_validation.rs | 2 +- .../program/tests/check_extensions.rs | 708 ++++++++++++++++++ 4 files changed, 729 insertions(+), 15 deletions(-) create mode 100644 programs/compressed-token/program/tests/check_extensions.rs diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs b/programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs index 2cffecf417..cf2db23fde 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/check_extensions.rs @@ -79,14 +79,15 @@ pub fn build_mint_extension_cache<'a>( packed_accounts: &'a ProgramPackedAccounts<'a, AccountInfo>, ) -> Result { let mut cache: MintExtensionCache = ArrayMap::new(); - let deny_restricted_extensions = !inputs.out_token_data.is_empty(); + let no_compressed_outputs = inputs.out_token_data.is_empty(); + let deny_restricted_extensions = !no_compressed_outputs; // Collect mints from input token data for input in inputs.in_token_data.iter() { let mint_index = input.mint; if cache.get_by_key(&mint_index).is_none() { let mint_account = packed_accounts.get_u8(mint_index, "mint cache: input")?; - let checks = if inputs.out_token_data.is_empty() { + let checks = if no_compressed_outputs { // No outputs - bypass state checks (full decompress or transfer-only) parse_mint_extensions(mint_account)? } else { @@ -103,14 +104,9 @@ pub fn build_mint_extension_cache<'a>( if cache.get_by_key(&mint_index).is_none() { let mint_account = packed_accounts.get_u8(mint_index, "mint cache: compression")?; - let no_compressed_outputs = inputs.out_token_data.is_empty(); - let is_full_decompress = compression.mode.is_decompress() && no_compressed_outputs; - let checks = if compression.mode.is_compress_and_close() - || is_full_decompress - || no_compressed_outputs - { + let checks = if compression.mode.is_compress_and_close() || no_compressed_outputs { // Bypass extension state checks (paused, non-zero fees, non-nil transfer hook) - // when exiting compressed state: CompressAndClose, Decompress, or CToken→SPL + // when CompressAndClose, full Decompress, or CToken→SPL (compress and full decompress) parse_mint_extensions(mint_account)? } else { check_mint_extensions(mint_account, deny_restricted_extensions)? @@ -119,7 +115,7 @@ pub fn build_mint_extension_cache<'a>( // CompressAndClose with restricted extensions requires CompressedOnly output. // Compress/Decompress don't need additional validation here: // - Compress: blocked by check_mint_extensions when outputs exist - // - Decompress: bypassed (restoring existing state) + // - Decompress: no check it restores existing state if checks.has_restricted_extensions && compression.mode.is_compress_and_close() { let output_idx = compression.get_compressed_token_account_index()?; let has_compressed_only = inputs @@ -144,5 +140,18 @@ pub fn build_mint_extension_cache<'a>( } } + for output in inputs.out_token_data.iter() { + // All mints of outputs that have non zero amount must have an input or compression. + // Thus we only check outputs with zero amounts here. + if output.amount.get() == 0 { + let mint_index = output.mint; + if cache.get_by_key(&mint_index).is_none() { + let mint_account = packed_accounts.get_u8(mint_index, "mint cache: output")?; + let checks = check_mint_extensions(mint_account, true)?; + cache.insert(mint_index, checks, ErrorCode::MintCacheCapacityExceeded)?; + } + } + } + Ok(cache) } diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs index e01db37136..60d237757f 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs @@ -101,8 +101,6 @@ pub fn process_token_compression<'a>( )?; } SPL_TOKEN_ID => { - // SPL Token (not Token-2022) never has restricted extensions. - // Delegation is disregarded for decompression to SPL token accounts. spl::process_spl_compressions( compression, &SPL_TOKEN_ID.to_pubkey_bytes(), @@ -122,8 +120,7 @@ pub fn process_token_compression<'a>( return Err(ErrorCode::CompressedOnlyRequiresCTokenDecompress.into()); } - // Check if mint has restricted extensions from the cache. - // Delegation is disregarded for decompression to SPL token accounts. + // Propagate whether mint is restricted to enable correct derivation of the spl interface pda. let is_restricted = mint_checks .map(|checks| checks.has_restricted_extensions) .unwrap_or(false); diff --git a/programs/compressed-token/program/src/shared/owner_validation.rs b/programs/compressed-token/program/src/shared/owner_validation.rs index e983743187..ea0e5a42e1 100644 --- a/programs/compressed-token/program/src/shared/owner_validation.rs +++ b/programs/compressed-token/program/src/shared/owner_validation.rs @@ -103,7 +103,7 @@ pub fn check_ctoken_owner( if let Some(checks) = mint_checks { if let Some(permanent_delegate) = &checks.permanent_delegate { if pubkey_eq(authority_key, permanent_delegate) { - return Ok(()); // Permanent delegate can compress any account of this mint + return Ok(()); // Permanent delegate can (de)compress any account of this mint } } } diff --git a/programs/compressed-token/program/tests/check_extensions.rs b/programs/compressed-token/program/tests/check_extensions.rs new file mode 100644 index 0000000000..d588950f76 --- /dev/null +++ b/programs/compressed-token/program/tests/check_extensions.rs @@ -0,0 +1,708 @@ +//! Specific unit tests for build_mint_extension_cache and check_mint_extensions. +//! +//! Tests are organized into categories: +//! - Category 1: Failure tests for MintHasRestrictedExtensions +//! - Category 2: Failure tests for CompressAndClose +//! - Category 3: Success tests for bypass scenarios +//! - Category 4: Success tests for non-restricted mints +//! - Category 5: Direct check_mint_extensions tests + +use anchor_compressed_token::ErrorCode; +use anchor_lang::prelude::ProgramError; +use anchor_lang::solana_program::pubkey::Pubkey as SolanaPubkey; +use light_account_checks::{ + account_info::test_account_info::pinocchio::get_account_info, + packed_accounts::ProgramPackedAccounts, +}; +use light_compressed_token::{ + compressed_token::transfer2::check_extensions::build_mint_extension_cache, + extensions::check_mint_extensions, +}; +use light_ctoken_interface::instructions::{ + extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData}, + transfer2::{ + Compression, CompressionMode, CompressedTokenInstructionDataTransfer2, + MultiInputTokenDataWithContext, MultiTokenTransferOutputData, + }, +}; +use light_zero_copy::traits::ZeroCopyAt; +use pinocchio::pubkey::Pubkey; +use spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodBool}; +use spl_token_2022::{ + extension::{ + metadata_pointer::MetadataPointer, pausable::PausableConfig, + permanent_delegate::PermanentDelegate, transfer_fee::TransferFeeConfig, + transfer_hook::TransferHook, BaseStateWithExtensionsMut, ExtensionType, + PodStateWithExtensionsMut, + }, + pod::PodMint, +}; + +const ANCHOR_ERROR_OFFSET: u32 = 6000; +const SPL_TOKEN_2022_ID: [u8; 32] = spl_token_2022::ID.to_bytes(); +const SPL_TOKEN_ID: [u8; 32] = spl_token::ID.to_bytes(); + +// ============================================================================ +// Helpers +// ============================================================================ + +/// Configuration for creating mock T22 mint with extensions. +#[derive(Default, Clone)] +struct MintConfig { + pub has_pausable: bool, + pub is_paused: bool, + pub has_transfer_fee: bool, + pub has_non_zero_fee: bool, + pub has_transfer_hook: bool, + pub has_non_nil_hook: bool, + pub has_permanent_delegate: bool, + pub has_metadata_pointer: bool, +} + +/// Create mock T22 mint data with specified extensions. +fn create_mock_t22_mint(config: &MintConfig) -> Vec { + use spl_token_2022::pod::PodCOption; + + let mut extensions = vec![]; + if config.has_pausable { + extensions.push(ExtensionType::Pausable); + } + if config.has_transfer_fee { + extensions.push(ExtensionType::TransferFeeConfig); + } + if config.has_transfer_hook { + extensions.push(ExtensionType::TransferHook); + } + if config.has_permanent_delegate { + extensions.push(ExtensionType::PermanentDelegate); + } + if config.has_metadata_pointer { + extensions.push(ExtensionType::MetadataPointer); + } + + let space = ExtensionType::try_calculate_account_len::(&extensions).unwrap(); + let mut data = vec![0u8; space]; + + let mut mint_state = + PodStateWithExtensionsMut::::unpack_uninitialized(&mut data).unwrap(); + + // Initialize base mint + mint_state.base.mint_authority = PodCOption::some(SolanaPubkey::new_unique()); + mint_state.base.decimals = 9; + mint_state.base.is_initialized = true.into(); + mint_state.base.freeze_authority = PodCOption::none(); + mint_state.base.supply = 1_000_000u64.into(); + mint_state.init_account_type().unwrap(); + + // Initialize extensions + if config.has_pausable { + let ext = mint_state + .init_extension::(true) + .unwrap(); + ext.authority = + OptionalNonZeroPubkey::try_from(Some(SolanaPubkey::new_unique())).unwrap(); + ext.paused = PodBool::from(config.is_paused); + } + + if config.has_transfer_fee { + let ext = mint_state + .init_extension::(true) + .unwrap(); + ext.transfer_fee_config_authority = + OptionalNonZeroPubkey::try_from(Some(SolanaPubkey::new_unique())).unwrap(); + ext.withdraw_withheld_authority = + OptionalNonZeroPubkey::try_from(Some(SolanaPubkey::new_unique())).unwrap(); + if config.has_non_zero_fee { + ext.older_transfer_fee.transfer_fee_basis_points = 100u16.into(); + ext.older_transfer_fee.maximum_fee = 1000u64.into(); + ext.newer_transfer_fee.transfer_fee_basis_points = 100u16.into(); + ext.newer_transfer_fee.maximum_fee = 1000u64.into(); + } + } + + if config.has_transfer_hook { + let ext = mint_state.init_extension::(true).unwrap(); + if config.has_non_nil_hook { + ext.program_id = + OptionalNonZeroPubkey::try_from(Some(SolanaPubkey::new_unique())).unwrap(); + } + } + + if config.has_permanent_delegate { + let ext = mint_state + .init_extension::(true) + .unwrap(); + ext.delegate = + OptionalNonZeroPubkey::try_from(Some(SolanaPubkey::new_unique())).unwrap(); + } + + if config.has_metadata_pointer { + let ext = mint_state + .init_extension::(true) + .unwrap(); + ext.metadata_address = + OptionalNonZeroPubkey::try_from(Some(SolanaPubkey::new_unique())).unwrap(); + } + + data +} + +/// Create mock SPL Token (non-T22) mint data. +fn create_mock_spl_token_mint() -> Vec { + // SPL Token mint is 82 bytes + let mut data = vec![0u8; 82]; + // Set is_initialized = true (offset 45, 1 byte) + data[45] = 1; + // Set decimals (offset 44) + data[44] = 9; + data +} + +/// Test configuration for instruction data. +#[derive(Default)] +struct TestConfig { + pub has_inputs: bool, + pub has_outputs: bool, + pub has_compressions: bool, + pub compression_mode: Option, + pub has_compressed_only_in_output: bool, + pub output_amount: u64, +} + +/// Create serialized instruction data for testing. +fn create_test_inputs(config: &TestConfig) -> Vec { + let in_token_data = if config.has_inputs { + vec![MultiInputTokenDataWithContext { + mint: 0, + amount: 100, + ..Default::default() + }] + } else { + vec![] + }; + + let out_token_data = if config.has_outputs { + vec![MultiTokenTransferOutputData { + mint: 0, + amount: config.output_amount, + ..Default::default() + }] + } else { + vec![] + }; + + let compressions = if config.has_compressions { + Some(vec![Compression { + mode: config.compression_mode.unwrap_or(CompressionMode::Compress), + amount: 100, + mint: 0, + source_or_recipient: 1, + authority: 2, + pool_account_index: 0, + pool_index: 0, + bump: 0, + decimals: 0, + }]) + } else { + None + }; + + let out_tlv = if config.has_outputs && config.has_compressed_only_in_output { + Some(vec![vec![ExtensionInstructionData::CompressedOnly( + CompressedOnlyExtensionInstructionData { + delegated_amount: 0, + withheld_transfer_fee: 0, + is_frozen: false, + compression_index: 0, + is_ata: false, + bump: 0, + owner_index: 0, + }, + )]]) + } else if config.has_outputs { + Some(vec![vec![]]) // Empty TLV for each output + } else { + None + }; + + let instruction_data = CompressedTokenInstructionDataTransfer2 { + with_transaction_hash: false, + with_lamports_change_account_merkle_tree_index: false, + lamports_change_account_merkle_tree_index: 0, + lamports_change_account_owner_index: 0, + output_queue: 0, + max_top_up: 0, + cpi_context: None, + compressions, + proof: None, + in_token_data, + out_token_data, + in_lamports: None, + out_lamports: None, + in_tlv: None, + out_tlv, + }; + + borsh::to_vec(&instruction_data).unwrap() +} + +/// Run build_mint_extension_cache with test data. +fn run_build_cache_test( + serialized_inputs: &[u8], + mint_data: &[u8], + owner: [u8; 32], +) -> Result<(), ProgramError> { + let (inputs, _) = + CompressedTokenInstructionDataTransfer2::zero_copy_at(serialized_inputs).unwrap(); + + let mint_account = get_account_info( + Pubkey::from(owner), + owner, + false, + false, + false, + mint_data.to_vec(), + ); + + let accounts = [mint_account]; + let packed_accounts = ProgramPackedAccounts { accounts: &accounts }; + + build_mint_extension_cache(&inputs, &packed_accounts).map(|_| ()) +} + +/// Helper to assert specific error code. +fn assert_error(result: Result<(), ProgramError>, expected: ErrorCode) { + let expected_code = ANCHOR_ERROR_OFFSET + expected as u32; + assert!( + matches!(result, Err(ProgramError::Custom(code)) if code == expected_code), + "Expected error {:?} (code {}), got {:?}", + expected, + expected_code, + result + ); +} + +// ============================================================================ +// Category 1: Failure Cases - MintHasRestrictedExtensions +// ============================================================================ + +#[test] +fn test_input_with_pausable_extension_fails() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_pausable: true, + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_inputs: true, + has_outputs: true, + output_amount: 100, + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert_error(result, ErrorCode::MintHasRestrictedExtensions); +} + +#[test] +fn test_input_with_permanent_delegate_extension_fails() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_permanent_delegate: true, + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_inputs: true, + has_outputs: true, + output_amount: 100, + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert_error(result, ErrorCode::MintHasRestrictedExtensions); +} + +#[test] +fn test_input_with_transfer_fee_extension_fails() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_transfer_fee: true, + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_inputs: true, + has_outputs: true, + output_amount: 100, + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert_error(result, ErrorCode::MintHasRestrictedExtensions); +} + +#[test] +fn test_input_with_transfer_hook_extension_fails() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_transfer_hook: true, + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_inputs: true, + has_outputs: true, + output_amount: 100, + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert_error(result, ErrorCode::MintHasRestrictedExtensions); +} + +#[test] +fn test_compress_with_pausable_extension_fails() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_pausable: true, + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_compressions: true, + compression_mode: Some(CompressionMode::Compress), + has_outputs: true, + output_amount: 100, + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert_error(result, ErrorCode::MintHasRestrictedExtensions); +} + +#[test] +fn test_decompress_with_pausable_extension_fails() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_pausable: true, + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_compressions: true, + compression_mode: Some(CompressionMode::Decompress), + has_outputs: true, + output_amount: 100, + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert_error(result, ErrorCode::MintHasRestrictedExtensions); +} + +#[test] +fn test_zero_amount_output_with_restricted_extension_fails() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_pausable: true, + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_outputs: true, + output_amount: 0, // Zero amount output + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert_error(result, ErrorCode::MintHasRestrictedExtensions); +} + +// ============================================================================ +// Category 2: Failure Cases - CompressAndClose +// ============================================================================ + +#[test] +fn test_compress_and_close_missing_compressed_only_fails() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_pausable: true, // Restricted extension + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_compressions: true, + compression_mode: Some(CompressionMode::CompressAndClose), + has_outputs: true, + has_compressed_only_in_output: false, // Missing CompressedOnly + output_amount: 100, + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert_error(result, ErrorCode::CompressAndCloseMissingCompressedOnlyExtension); +} + +#[test] +fn test_compress_and_close_empty_tlv_fails() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_permanent_delegate: true, // Different restricted extension + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_compressions: true, + compression_mode: Some(CompressionMode::CompressAndClose), + has_outputs: true, + has_compressed_only_in_output: false, // Empty TLV + output_amount: 100, + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert_error(result, ErrorCode::CompressAndCloseMissingCompressedOnlyExtension); +} + +// ============================================================================ +// Category 3: Success Cases - Bypass Scenarios +// ============================================================================ + +#[test] +fn test_input_with_restricted_no_outputs_succeeds() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_pausable: true, // Restricted, but no outputs = bypass + is_paused: true, // Even paused is OK with bypass + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_inputs: true, + has_outputs: false, // No outputs = bypass + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert!(result.is_ok(), "Should succeed with bypass, got {:?}", result); +} + +#[test] +fn test_compress_and_close_with_compressed_only_succeeds() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_pausable: true, + is_paused: true, // Even paused is OK for CompressAndClose + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_compressions: true, + compression_mode: Some(CompressionMode::CompressAndClose), + has_outputs: true, + has_compressed_only_in_output: true, // Has required extension + output_amount: 100, + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert!(result.is_ok(), "Should succeed with CompressedOnly, got {:?}", result); +} + +#[test] +fn test_decompress_no_outputs_succeeds() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_transfer_fee: true, + has_non_zero_fee: true, // Would fail if checked + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_compressions: true, + compression_mode: Some(CompressionMode::Decompress), + has_outputs: false, // No outputs = bypass + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert!(result.is_ok(), "Should succeed with bypass, got {:?}", result); +} + +#[test] +fn test_compress_no_outputs_succeeds() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_transfer_hook: true, + has_non_nil_hook: true, // Would fail if checked + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_compressions: true, + compression_mode: Some(CompressionMode::Compress), + has_outputs: false, // No outputs = bypass + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert!(result.is_ok(), "Should succeed with bypass, got {:?}", result); +} + +// ============================================================================ +// Category 4: Success Cases - Non-Restricted Mints +// ============================================================================ + +#[test] +fn test_spl_token_mint_succeeds() { + let mint_data = create_mock_spl_token_mint(); + let inputs = create_test_inputs(&TestConfig { + has_inputs: true, + has_outputs: true, + output_amount: 100, + ..Default::default() + }); + + // SPL Token mint is owned by spl_token::ID, not spl_token_2022::ID + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_ID); + assert!(result.is_ok(), "SPL Token should succeed, got {:?}", result); +} + +#[test] +fn test_t22_mint_no_extensions_succeeds() { + let mint_data = create_mock_t22_mint(&MintConfig::default()); // No extensions + let inputs = create_test_inputs(&TestConfig { + has_inputs: true, + has_outputs: true, + output_amount: 100, + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert!(result.is_ok(), "T22 without extensions should succeed, got {:?}", result); +} + +#[test] +fn test_t22_mint_with_metadata_only_succeeds() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_metadata_pointer: true, // Not a restricted extension + ..Default::default() + }); + let inputs = create_test_inputs(&TestConfig { + has_inputs: true, + has_outputs: true, + output_amount: 100, + ..Default::default() + }); + + let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); + assert!(result.is_ok(), "MetadataPointer should succeed, got {:?}", result); +} + +// ============================================================================ +// Category 5: Direct check_mint_extensions Tests +// ============================================================================ + +#[test] +fn test_check_mint_extensions_paused_mint() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_pausable: true, + is_paused: true, + ..Default::default() + }); + let mint_account = get_account_info( + Pubkey::from(SPL_TOKEN_2022_ID), + SPL_TOKEN_2022_ID, + false, + false, + false, + mint_data, + ); + + // Call check_mint_extensions directly with deny_restricted=false + // This bypasses the restricted check and reaches the paused check + let result = check_mint_extensions(&mint_account, false); + assert_error(result.map(|_| ()), ErrorCode::MintPaused); +} + +#[test] +fn test_check_mint_extensions_non_zero_fee() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_transfer_fee: true, + has_non_zero_fee: true, + ..Default::default() + }); + let mint_account = get_account_info( + Pubkey::from(SPL_TOKEN_2022_ID), + SPL_TOKEN_2022_ID, + false, + false, + false, + mint_data, + ); + + let result = check_mint_extensions(&mint_account, false); + assert_error(result.map(|_| ()), ErrorCode::NonZeroTransferFeeNotSupported); +} + +#[test] +fn test_check_mint_extensions_non_nil_hook() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_transfer_hook: true, + has_non_nil_hook: true, + ..Default::default() + }); + let mint_account = get_account_info( + Pubkey::from(SPL_TOKEN_2022_ID), + SPL_TOKEN_2022_ID, + false, + false, + false, + mint_data, + ); + + let result = check_mint_extensions(&mint_account, false); + assert_error(result.map(|_| ()), ErrorCode::TransferHookNotSupported); +} + +#[test] +fn test_check_mint_extensions_deny_restricted_fails() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_pausable: true, // Restricted extension + is_paused: false, // Not paused, but still restricted + ..Default::default() + }); + let mint_account = get_account_info( + Pubkey::from(SPL_TOKEN_2022_ID), + SPL_TOKEN_2022_ID, + false, + false, + false, + mint_data, + ); + + // deny_restricted=true should fail even if mint state is valid + let result = check_mint_extensions(&mint_account, true); + assert_error(result.map(|_| ()), ErrorCode::MintHasRestrictedExtensions); +} + +#[test] +fn test_check_mint_extensions_deny_restricted_non_restricted_succeeds() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_metadata_pointer: true, // Not a restricted extension + ..Default::default() + }); + let mint_account = get_account_info( + Pubkey::from(SPL_TOKEN_2022_ID), + SPL_TOKEN_2022_ID, + false, + false, + false, + mint_data, + ); + + // deny_restricted=true should succeed with non-restricted mint + let result = check_mint_extensions(&mint_account, true); + assert!(result.is_ok(), "Non-restricted mint should succeed, got {:?}", result); +} + +#[test] +fn test_check_mint_extensions_valid_mint_succeeds() { + let mint_data = create_mock_t22_mint(&MintConfig { + has_pausable: true, + is_paused: false, // Not paused + has_transfer_fee: true, + has_non_zero_fee: false, // Zero fee + has_transfer_hook: true, + has_non_nil_hook: false, // Nil hook + ..Default::default() + }); + let mint_account = get_account_info( + Pubkey::from(SPL_TOKEN_2022_ID), + SPL_TOKEN_2022_ID, + false, + false, + false, + mint_data, + ); + + // deny_restricted=false with all valid states should succeed + let result = check_mint_extensions(&mint_account, false); + assert!(result.is_ok(), "Valid mint should succeed, got {:?}", result); +} From e3b7bdb508a75d11b00776bbadd2951ccb936384 Mon Sep 17 00:00:00 2001 From: ananas Date: Wed, 7 Jan 2026 00:29:40 +0000 Subject: [PATCH 12/18] refactor: remove CreateSplMint dead code and cleanup - Remove CreateSplMint enum variant and CreateSplMintAction struct (never activated) - Remove create_spl_mint.rs file from ctoken-interface - Add MAX_COMPRESSIONS constant (32) with meaningful error message - Remove dead no_output_compressed_accounts field from Transfer2Config - Update documentation (MINT_ACTION.md, CLAUDE.md, lib.rs) - Update JS layout to remove CreateSplMint - Refactor check_extensions tests for better coverage --- .../src/v3/layout/layout-mint-action.ts | 8 -- .../mint_action/create_spl_mint.rs | 9 -- .../mint_action/instruction_data.rs | 9 +- .../src/instructions/mint_action/mod.rs | 2 - .../tests/mint/functional.rs | 33 +------ programs/compressed-token/anchor/src/lib.rs | 2 +- programs/compressed-token/program/CLAUDE.md | 2 +- .../docs/compressed_token/MINT_ACTION.md | 33 +++---- .../compressed_token/mint_action/accounts.rs | 30 ++----- .../mint_action/actions/process_actions.rs | 11 --- .../compressed_token/mint_action/processor.rs | 9 +- .../transfer2/compression/ctoken/inputs.rs | 4 +- .../transfer2/compression/mod.rs | 15 ++-- .../src/compressed_token/transfer2/config.rs | 4 - .../transfer2/token_inputs.rs | 8 +- .../program/src/ctoken/approve_revoke.rs | 3 +- .../program/src/ctoken/transfer/checked.rs | 7 +- programs/compressed-token/program/src/lib.rs | 14 +-- .../program/src/shared/mint_to_token_pool.rs | 2 +- .../program/src/shared/mod.rs | 4 +- .../program/src/shared/token_input.rs | 1 - .../program/tests/check_extensions.rs | 86 +++++++++++++------ .../program/tests/mint_action.rs | 42 +++------ .../v2/mint_action/account_metas.rs | 4 +- .../token-client/src/actions/mint_action.rs | 4 +- 25 files changed, 137 insertions(+), 209 deletions(-) delete mode 100644 program-libs/ctoken-interface/src/instructions/mint_action/create_spl_mint.rs diff --git a/js/compressed-token/src/v3/layout/layout-mint-action.ts b/js/compressed-token/src/v3/layout/layout-mint-action.ts index 6e5ac89456..ed75d7ea6c 100644 --- a/js/compressed-token/src/v3/layout/layout-mint-action.ts +++ b/js/compressed-token/src/v3/layout/layout-mint-action.ts @@ -37,8 +37,6 @@ export const UpdateAuthorityLayout = struct([ option(publicKey(), 'newAuthority'), ]); -export const CreateSplMintActionLayout = struct([u8('mintBump')]); - export const MintToCTokenActionLayout = struct([ u8('accountIndex'), u64('amount'), @@ -74,7 +72,6 @@ export const ActionLayout = rustEnum([ MintToCompressedActionLayout.replicate('mintToCompressed'), UpdateAuthorityLayout.replicate('updateMintAuthority'), UpdateAuthorityLayout.replicate('updateFreezeAuthority'), - CreateSplMintActionLayout.replicate('createSplMint'), MintToCTokenActionLayout.replicate('mintToCToken'), UpdateMetadataFieldActionLayout.replicate('updateMetadataField'), UpdateMetadataAuthorityActionLayout.replicate('updateMetadataAuthority'), @@ -233,10 +230,6 @@ export interface UpdateAuthority { newAuthority: PublicKey | null; } -export interface CreateSplMintAction { - mintBump: number; -} - export interface MintToCTokenAction { accountIndex: number; amount: bigint; @@ -274,7 +267,6 @@ export type Action = | { mintToCompressed: MintToCompressedAction } | { updateMintAuthority: UpdateAuthority } | { updateFreezeAuthority: UpdateAuthority } - | { createSplMint: CreateSplMintAction } | { mintToCToken: MintToCTokenAction } | { updateMetadataField: UpdateMetadataFieldAction } | { updateMetadataAuthority: UpdateMetadataAuthorityAction } diff --git a/program-libs/ctoken-interface/src/instructions/mint_action/create_spl_mint.rs b/program-libs/ctoken-interface/src/instructions/mint_action/create_spl_mint.rs deleted file mode 100644 index 8b141eaad6..0000000000 --- a/program-libs/ctoken-interface/src/instructions/mint_action/create_spl_mint.rs +++ /dev/null @@ -1,9 +0,0 @@ -use light_zero_copy::ZeroCopy; - -use crate::{AnchorDeserialize, AnchorSerialize}; - -#[repr(C)] -#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)] -pub struct CreateSplMintAction { - pub mint_bump: u8, -} diff --git a/program-libs/ctoken-interface/src/instructions/mint_action/instruction_data.rs b/program-libs/ctoken-interface/src/instructions/mint_action/instruction_data.rs index 9f4cab62f6..890b8c4fd4 100644 --- a/program-libs/ctoken-interface/src/instructions/mint_action/instruction_data.rs +++ b/program-libs/ctoken-interface/src/instructions/mint_action/instruction_data.rs @@ -3,8 +3,8 @@ use light_compressible::compression_info::CompressionInfo; use light_zero_copy::ZeroCopy; use super::{ - CompressAndCloseCMintAction, CpiContext, CreateSplMintAction, DecompressMintAction, - MintToCTokenAction, MintToCompressedAction, RemoveMetadataKeyAction, UpdateAuthority, + CompressAndCloseCMintAction, CpiContext, DecompressMintAction, MintToCTokenAction, + MintToCompressedAction, RemoveMetadataKeyAction, UpdateAuthority, UpdateMetadataAuthorityAction, UpdateMetadataFieldAction, }; use crate::{ @@ -25,11 +25,6 @@ pub enum Action { UpdateMintAuthority(UpdateAuthority), /// Update freeze authority of a compressed mint account. UpdateFreezeAuthority(UpdateAuthority), - /// Create an spl mint for a cmint. - /// - existing supply is minted to a token pool account. - /// - mint and freeze authority are a ctoken pda. - /// - is an spl-token-2022 mint account. - CreateSplMint(CreateSplMintAction), /// Mint ctokens from a cmint to a ctoken solana account /// (tokens are not compressed but not spl tokens). MintToCToken(MintToCTokenAction), diff --git a/program-libs/ctoken-interface/src/instructions/mint_action/mod.rs b/program-libs/ctoken-interface/src/instructions/mint_action/mod.rs index d9e4de2a52..7ba2438bfd 100644 --- a/program-libs/ctoken-interface/src/instructions/mint_action/mod.rs +++ b/program-libs/ctoken-interface/src/instructions/mint_action/mod.rs @@ -1,7 +1,6 @@ mod builder; mod compress_and_close_cmint; mod cpi_context; -mod create_spl_mint; mod decompress_mint; mod instruction_data; mod mint_to_compressed; @@ -11,7 +10,6 @@ mod update_mint; pub use compress_and_close_cmint::*; pub use cpi_context::*; -pub use create_spl_mint::*; pub use decompress_mint::*; pub use instruction_data::*; pub use mint_to_compressed::*; diff --git a/program-tests/compressed-token-test/tests/mint/functional.rs b/program-tests/compressed-token-test/tests/mint/functional.rs index 5c45f9cb65..37211d0bdd 100644 --- a/program-tests/compressed-token-test/tests/mint/functional.rs +++ b/program-tests/compressed-token-test/tests/mint/functional.rs @@ -156,37 +156,8 @@ async fn test_create_compressed_mint() { ) .await; } - // // 3. Create SPL mint from compressed mint - // // Get compressed mint data before creating SPL mint - // { - // let pre_compressed_mint_account = rpc - // .indexer() - // .unwrap() - // .get_compressed_account(compressed_mint_address, None) - // .await - // .unwrap() - // .value.unwrap(); - // let pre_compressed_mint: CompressedMint = BorshDeserialize::deserialize( - // &mut pre_compressed_mint_account.data.unwrap().data.as_slice(), - // ) - // .unwrap(); - - // // Use our create_spl_mint action helper (automatically handles proofs, PDAs, and transaction) - // create_spl_mint( - // &mut rpc, - // compressed_mint_address, - // &mint_seed, - // &mint_authority_keypair, - // &payer, - // ) - // .await - // .unwrap(); - - // // Verify SPL mint was created using our assertion helper - // assert_spl_mint(&mut rpc, mint_seed.pubkey(), &pre_compressed_mint).await; - // } - // 4. Transfer compressed tokens to new recipient + // 3. Transfer compressed tokens to new recipient // Get the compressed token account for decompression let compressed_token_accounts = rpc .indexer() @@ -1256,7 +1227,7 @@ async fn test_mint_actions() { metadata: CompressedMintMetadata { version: 3, // With metadata mint: spl_mint_pda.into(), - cmint_decompressed: false, // Should be true after CreateSplMint action + cmint_decompressed: false, // Becomes true after DecompressMint action }, reserved: [0u8; 49], account_type: ACCOUNT_TYPE_MINT, diff --git a/programs/compressed-token/anchor/src/lib.rs b/programs/compressed-token/anchor/src/lib.rs index 33346ab26b..d74e90eb77 100644 --- a/programs/compressed-token/anchor/src/lib.rs +++ b/programs/compressed-token/anchor/src/lib.rs @@ -425,7 +425,7 @@ pub enum ErrorCode { CompressAndCloseInvalidVersion, // 6093 #[msg("InvalidAddressTree")] InvalidAddressTree, // 6094 - #[msg("Too many compression transfers. Maximum 40 transfers allowed per instruction")] + #[msg("Too many compression transfers. Maximum 32 transfers allowed per instruction")] TooManyCompressionTransfers, // 6095 #[msg("Missing fee payer for compressions-only operation")] CompressionsOnlyMissingFeePayer, // 6096 diff --git a/programs/compressed-token/program/CLAUDE.md b/programs/compressed-token/program/CLAUDE.md index 9502adc282..99290c34c5 100644 --- a/programs/compressed-token/program/CLAUDE.md +++ b/programs/compressed-token/program/CLAUDE.md @@ -73,7 +73,7 @@ Every instruction description must include the sections: 6. **MintAction** - [`docs/instructions/MINT_ACTION.md`](docs/instructions/MINT_ACTION.md) - Batch instruction for compressed mint management and mint operations (discriminator: 103, enum: `InstructionType::MintAction`) - - Supports 9 action types: CreateCompressedMint, MintTo, UpdateMintAuthority, UpdateFreezeAuthority, CreateSplMint, MintToCToken, UpdateMetadataField, UpdateMetadataAuthority, RemoveMetadataKey + - Supports 10 action types: CreateCompressedMint, MintTo, UpdateMintAuthority, UpdateFreezeAuthority, MintToCToken, UpdateMetadataField, UpdateMetadataAuthority, RemoveMetadataKey, DecompressMint, CompressAndCloseCMint - Handles both compressed and decompressed token minting 7. **CTokenTransfer** - [`docs/instructions/CTOKEN_TRANSFER.md`](docs/instructions/CTOKEN_TRANSFER.md) diff --git a/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md b/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md index 3e6c045b87..eb44dcae7c 100644 --- a/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md +++ b/programs/compressed-token/program/docs/compressed_token/MINT_ACTION.md @@ -7,7 +7,7 @@ **description:** Batch instruction for managing compressed mint accounts (cmints) and performing mint operations. A compressed mint account stores the mint's supply, decimals, authorities (mint/freeze), and optional TokenMetadata extension in compressed state. TokenMetadata is the only extension supported for compressed mints and provides fields for name, symbol, uri, update_authority, and additional key-value metadata. -This instruction supports 11 total actions - one creation action (controlled by `create_mint` flag) and 10 enum-based actions: +This instruction supports 10 total actions - one creation action (controlled by `create_mint` flag) and 9 enum-based actions: **Compressed mint creation (executed first when `create_mint` is Some):** 1. **Create Compressed Mint** - Create a new compressed mint account with initial authorities and optional TokenMetadata extension @@ -15,20 +15,19 @@ This instruction supports 11 total actions - one creation action (controlled by **Core mint operations (Action enum variants):** 2. `MintToCompressed` - Mint new compressed tokens to one or more compressed token accounts 3. `MintToCToken` - Mint new tokens to decompressed ctoken accounts (not SPL tokens) -4. `CreateSplMint` - Create an SPL Token 2022 mint for an existing compressed mint, enabling SPL interoperability **Authority updates (Action enum variants):** -5. `UpdateMintAuthority` - Update or remove the mint authority -6. `UpdateFreezeAuthority` - Update or remove the freeze authority +4. `UpdateMintAuthority` - Update or remove the mint authority +5. `UpdateFreezeAuthority` - Update or remove the freeze authority **TokenMetadata extension operations (Action enum variants):** -7. `UpdateMetadataField` - Update name, symbol, uri, or additional_metadata fields in the TokenMetadata extension -8. `UpdateMetadataAuthority` - Update the metadata update authority in the TokenMetadata extension -9. `RemoveMetadataKey` - Remove a key-value pair from additional_metadata in the TokenMetadata extension +6. `UpdateMetadataField` - Update name, symbol, uri, or additional_metadata fields in the TokenMetadata extension +7. `UpdateMetadataAuthority` - Update the metadata update authority in the TokenMetadata extension +8. `RemoveMetadataKey` - Remove a key-value pair from additional_metadata in the TokenMetadata extension **Decompress/Compress operations (Action enum variants):** -10. `DecompressMint` - Decompress a compressed mint to a CMint Solana account. Creates a CMint PDA that becomes the source of truth. -11. `CompressAndCloseCMint` - Compress and close a CMint Solana account. Permissionless - anyone can call if is_compressible() returns true (rent expired). +9. `DecompressMint` - Decompress a compressed mint to a CMint Solana account. Creates a CMint PDA that becomes the source of truth. +10. `CompressAndCloseCMint` - Compress and close a CMint Solana account. Permissionless - anyone can call if is_compressible() returns true (rent expired). Key concepts integrated: - **Compressed mint (cmint)**: Mint state stored in compressed account with deterministic address derived from associated SPL mint pubkey @@ -54,16 +53,15 @@ Key concepts integrated: - `mint`: Option - Full mint state including supply, decimals, metadata, authorities, and extensions (None when reading from decompressed CMint) 2. Action types (path: program-libs/ctoken-interface/src/instructions/mint_action/): - - `MintToCompressed(MintToCompressedAction)` - Mint tokens to compressed accounts (mint_to.rs) + - `MintToCompressed(MintToCompressedAction)` - Mint tokens to compressed accounts (mint_to_compressed.rs) - `UpdateMintAuthority(UpdateAuthority)` - Update mint authority (update_mint.rs) - `UpdateFreezeAuthority(UpdateAuthority)` - Update freeze authority (update_mint.rs) - - `CreateSplMint(CreateSplMintAction)` - Create SPL mint for cmint (create_spl_mint.rs) - `MintToCToken(MintToCTokenAction)` - Mint to ctoken accounts (mint_to_ctoken.rs) - `UpdateMetadataField(UpdateMetadataFieldAction)` - Update metadata field (update_metadata.rs) - `UpdateMetadataAuthority(UpdateMetadataAuthorityAction)` - Update metadata authority (update_metadata.rs) - `RemoveMetadataKey(RemoveMetadataKeyAction)` - Remove metadata key (update_metadata.rs) - - `DecompressMint(DecompressMintAction)` - Decompress compressed mint to CMint Solana account - - `CompressAndCloseCMint(CompressAndCloseCMintAction)` - Compress and close CMint Solana account + - `DecompressMint(DecompressMintAction)` - Decompress compressed mint to CMint Solana account (decompress_mint.rs) + - `CompressAndCloseCMint(CompressAndCloseCMintAction)` - Compress and close CMint Solana account (compress_and_close_cmint.rs) **Accounts:** 1. light_system_program @@ -72,7 +70,7 @@ Key concepts integrated: Optional accounts (based on configuration): 2. mint_signer - - (signer) - required if create_mint is Some or CreateSplMint action present + - (signer) - required if create_mint is Some or DecompressMint action present - PDA seed for SPL mint creation (seeds from compressed mint randomness) 3. authority @@ -161,13 +159,6 @@ Packed accounts (remaining accounts): - Validate: current authority matches signer - Update: set new authority (can be None to disable) - **CreateSplMint:** - - Validate: mint_signer is provided and signing - - Create: SPL Token 2022 mint account via CPI - - Create: Token pool PDA account - - Initialize: mint with ctoken PDA as mint/freeze authority - - Mint: existing supply to token pool - **MintToCToken:** - Validate: mint authority matches signer - Calculate: sum recipient amounts diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs index 5e988aea56..61dfdde529 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs @@ -70,13 +70,11 @@ impl<'info> MintActionAccounts<'info> { accounts: &'info [AccountInfo], config: &AccountsConfig, cmint_pubkey: Option<&solana_pubkey::Pubkey>, - token_pool_index: u8, - token_pool_bump: u8, ) -> Result { let mut iter = AccountIterator::new(accounts); let light_system_program = iter.next_account("light_system_program")?; - // mint_signer needs to sign for create_mint/create_spl_mint, but not for decompress_mint + // mint_signer needs to sign for create_mint, but not for decompress_mint let mint_signer = if config.mint_signer_must_sign() { iter.next_option_signer("mint_signer", config.with_mint_signer)? } else { @@ -156,7 +154,7 @@ impl<'info> MintActionAccounts<'info> { accounts: iter.remaining_unchecked()?, }, }; - mint_accounts.validate_accounts(cmint_pubkey, token_pool_index, token_pool_bump)?; + mint_accounts.validate_accounts(cmint_pubkey)?; Ok(mint_accounts) } @@ -304,8 +302,6 @@ impl<'info> MintActionAccounts<'info> { pub fn validate_accounts( &self, cmint_pubkey: Option<&solana_pubkey::Pubkey>, - _token_pool_index: u8, //TODO: remove - _token_pool_bump: u8, ) -> Result<(), ProgramError> { let accounts = self .executing @@ -386,7 +382,7 @@ impl AccountsConfig { } /// Returns true if mint_signer must be a signer. - /// Required for create_mint and create_spl_mint, but NOT for decompress_mint. + /// Required for create_mint, but NOT for decompress_mint. /// decompress_mint only needs mint_signer.key() for PDA derivation. #[inline(always)] pub fn mint_signer_must_sign(&self) -> bool { @@ -418,11 +414,6 @@ impl AccountsConfig { .as_ref() .map(|x| x.first_set_context() || x.set_context()) .unwrap_or_default(); - // An action in this instruction creates a the spl mint corresponding to a compressed mint. - let create_spl_mint = parsed_instruction_data - .actions - .iter() - .any(|action| matches!(action, ZAction::CreateSplMint(_))); // Check if DecompressMint action is present let has_decompress_mint_action = parsed_instruction_data @@ -442,19 +433,14 @@ impl AccountsConfig { return Err(ErrorCode::CannotDecompressAndCloseInSameInstruction.into()); } - // We need mint signer if create mint, create spl mint, or decompress mint. + // We need mint signer if create mint or decompress mint. // CompressAndCloseCMint does NOT need mint_signer - it verifies CMint by compressed_mint.metadata.mint - let with_mint_signer = parsed_instruction_data.create_mint.is_some() - || create_spl_mint - || has_decompress_mint_action; + let with_mint_signer = + parsed_instruction_data.create_mint.is_some() || has_decompress_mint_action; // CMint account needed for sync when mint is already decompressed (metadata flag) // When mint is None, it means CMint is decompressed (data lives in CMint account) let cmint_decompressed = parsed_instruction_data.mint.is_none(); - if cmint_decompressed && create_spl_mint { - return Err(ProgramError::InvalidInstructionData); - } - if write_to_cpi_context { // Must not have any MintToCToken actions let has_mint_to_ctoken_actions = parsed_instruction_data @@ -465,10 +451,6 @@ impl AccountsConfig { msg!("Mint to ctokens not allowed when writing to cpi context"); return Err(ErrorCode::CpiContextSetNotUsable.into()); } - if create_spl_mint { - msg!("Create spl mint not allowed when writing to cpi context"); - return Err(ErrorCode::CpiContextSetNotUsable.into()); - } if has_decompress_mint_action || cmint_decompressed { msg!("Decompress mint not allowed when writing to cpi context"); return Err(ErrorCode::CpiContextSetNotUsable.into()); diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs index a4c91f0285..2527be3a99 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/process_actions.rs @@ -86,17 +86,6 @@ pub fn process_actions<'a>( compressed_mint.base.freeze_authority = update_action.new_authority.as_ref().map(|a| **a); } - // TODO: Remove CreateSplMint - dead code, never activated - ZAction::CreateSplMint(_create_spl_action) => { - return Err(ErrorCode::MintActionUnsupportedOperation.into()); - // process_create_spl_mint_action( - // create_spl_action, - // validated_accounts, - // &parsed_instruction_data.mint, - // parsed_instruction_data.token_pool_bump, - // )?; - // compressed_mint.metadata.cmint_decompressed = true; - } ZAction::MintToCToken(mint_to_ctoken_action) => { let account_index = mint_to_ctoken_action.account_index as usize; if account_index >= MAX_PACKED_ACCOUNTS { diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs index dfcbf64646..cb4f5e4466 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs @@ -38,13 +38,8 @@ pub fn process_mint_action( .as_ref() .map(|m| m.metadata.mint.into()); // Validate and parse - let validated_accounts = MintActionAccounts::validate_and_parse( - accounts, - &accounts_config, - cmint_pubkey.as_ref(), - parsed_instruction_data.token_pool_index, - parsed_instruction_data.token_pool_bump, - )?; + let validated_accounts = + MintActionAccounts::validate_and_parse(accounts, &accounts_config, cmint_pubkey.as_ref())?; // Get mint data based on source: // 1. Creating new mint: mint data required in instruction diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/inputs.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/inputs.rs index 5e44b3685f..8b2006220e 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/inputs.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/inputs.rs @@ -10,7 +10,7 @@ use light_ctoken_interface::instructions::{ use pinocchio::{account_info::AccountInfo, pubkey::Pubkey}; use spl_pod::solana_msg::msg; -use crate::extensions::MintExtensionChecks; +use crate::{extensions::MintExtensionChecks, MAX_COMPRESSIONS}; /// Decompress-specific inputs from the input compressed account. /// Only required for decompression with CompressedOnly extension. @@ -36,7 +36,7 @@ impl<'a> DecompressCompressOnlyInputs<'a> { pub fn try_extract( compression: &ZCompression, compression_index: usize, - compression_to_input: &[Option; 32], + compression_to_input: &[Option; MAX_COMPRESSIONS], inputs: &'a ZCompressedTokenInstructionDataTransfer2<'a>, packed_accounts: &'a ProgramPackedAccounts<'a, AccountInfo>, ) -> Result, ProgramError> { diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs index 60d237757f..cb4458e47f 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs @@ -19,7 +19,7 @@ use crate::{ convert_program_error, transfer_lamports::{multi_transfer_lamports, Transfer}, }, - LIGHT_CPI_SIGNER, MAX_PACKED_ACCOUNTS, + LIGHT_CPI_SIGNER, MAX_COMPRESSIONS, MAX_PACKED_ACCOUNTS, }; pub mod ctoken; @@ -47,13 +47,16 @@ pub fn process_token_compression<'a>( cpi_authority: &AccountInfo, max_top_up: u16, mint_cache: &'a MintExtensionCache, - compression_to_input: &[Option; 32], + compression_to_input: &[Option; MAX_COMPRESSIONS], ) -> Result<(), ProgramError> { if let Some(compressions) = inputs.compressions.as_ref() { - if compressions.len() >= 32 { - // TODO: add meaningful error message - // TODO: use constant instead of 32. - return Err(ProgramError::InvalidInstructionData); + if compressions.len() >= MAX_COMPRESSIONS { + msg!( + "Too many compressions: {} provided, maximum {} allowed", + compressions.len(), + MAX_COMPRESSIONS + ); + return Err(ErrorCode::TooManyCompressionTransfers.into()); } let mut transfer_map = [0u64; MAX_PACKED_ACCOUNTS]; // Initialize budget: +1 allows exact match (total == max_top_up) diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/config.rs b/programs/compressed-token/program/src/compressed_token/transfer2/config.rs index 9a8ad16266..a04c239ede 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/config.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/config.rs @@ -20,8 +20,6 @@ pub struct Transfer2Config { pub total_output_lamports: u64, /// No compressed accounts (neither input nor output) - determines system CPI path pub no_compressed_accounts: bool, - /// No output compressed accounts - determines mint extension hotpath - pub no_output_compressed_accounts: bool, // TODO: remove dead code } impl Transfer2Config { @@ -32,7 +30,6 @@ impl Transfer2Config { ) -> Result { let no_compressed_accounts = inputs.in_token_data.is_empty() && inputs.out_token_data.is_empty(); - let no_output_compressed_accounts = inputs.out_token_data.is_empty(); Ok(Self { sol_pool_required: false, sol_decompression_required: false, @@ -45,7 +42,6 @@ impl Transfer2Config { total_input_lamports: 0, total_output_lamports: 0, no_compressed_accounts, - no_output_compressed_accounts, }) } } diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/token_inputs.rs b/programs/compressed-token/program/src/compressed_token/transfer2/token_inputs.rs index fa85da81e2..bba5abb69b 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/token_inputs.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/token_inputs.rs @@ -12,10 +12,10 @@ use light_program_profiler::profile; use pinocchio::account_info::AccountInfo; use super::check_extensions::{validate_tlv_and_get_frozen, MintExtensionCache}; -use crate::shared::token_input::set_input_compressed_account; +use crate::{shared::token_input::set_input_compressed_account, MAX_COMPRESSIONS}; /// Process input compressed accounts and return compression-to-input lookup. -/// Returns `[Option; 32]` where `compression_to_input[compression_idx] = Some(input_idx)`. +/// Returns `[Option; MAX_COMPRESSIONS]` where `compression_to_input[compression_idx] = Some(input_idx)`. #[profile] #[inline(always)] pub fn set_input_compressed_accounts<'a>( @@ -25,9 +25,9 @@ pub fn set_input_compressed_accounts<'a>( packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, all_accounts: &[AccountInfo], mint_cache: &'a MintExtensionCache, -) -> Result<[Option; 32], ProgramError> { +) -> Result<[Option; MAX_COMPRESSIONS], ProgramError> { // compression_to_input[compression_index] = Some(input_index), None means unset - let mut compression_to_input: [Option; 32] = [None; 32]; + let mut compression_to_input: [Option; MAX_COMPRESSIONS] = [None; MAX_COMPRESSIONS]; for (i, input_data) in inputs.in_token_data.iter().enumerate() { let input_lamports = if let Some(lamports) = inputs.in_lamports.as_ref() { diff --git a/programs/compressed-token/program/src/ctoken/approve_revoke.rs b/programs/compressed-token/program/src/ctoken/approve_revoke.rs index af859dea89..eb8ceff21d 100644 --- a/programs/compressed-token/program/src/ctoken/approve_revoke.rs +++ b/programs/compressed-token/program/src/ctoken/approve_revoke.rs @@ -178,7 +178,8 @@ pub fn process_ctoken_approve_checked( } // Parse amount and decimals from instruction data - let (amount, decimals) = unpack_amount_and_decimals(instruction_data).map_err(convert_token_error)?; + let (amount, decimals) = + unpack_amount_and_decimals(instruction_data).map_err(convert_token_error)?; // SAFETY: accounts.len() >= 4 validated at function entry let source = &accounts[APPROVE_CHECKED_ACCOUNT_SOURCE]; diff --git a/programs/compressed-token/program/src/ctoken/transfer/checked.rs b/programs/compressed-token/program/src/ctoken/transfer/checked.rs index 20cf3863b3..3db6d122cd 100644 --- a/programs/compressed-token/program/src/ctoken/transfer/checked.rs +++ b/programs/compressed-token/program/src/ctoken/transfer/checked.rs @@ -7,7 +7,9 @@ use pinocchio_token_program::processor::{ }; use super::shared::{process_transfer_extensions_transfer_checked, TransferAccounts}; -use crate::shared::{convert_pinocchio_token_error, convert_token_error, owner_validation::check_token_program_owner}; +use crate::shared::{ + convert_pinocchio_token_error, convert_token_error, owner_validation::check_token_program_owner, +}; /// Account indices for CToken transfer_checked instruction /// Note: Different from ctoken_transfer - mint is at index 1 const ACCOUNT_SOURCE: usize = 0; @@ -75,7 +77,8 @@ pub fn process_ctoken_transfer_checked( )?; // Pass the first 9 bytes (amount + decimals) to the SPL transfer_checked processor - let (amount, decimals) = unpack_amount_and_decimals(instruction_data).map_err(convert_token_error)?; + let (amount, decimals) = + unpack_amount_and_decimals(instruction_data).map_err(convert_token_error)?; if let Some(extension_decimals) = extension_decimals { if extension_decimals != decimals { diff --git a/programs/compressed-token/program/src/lib.rs b/programs/compressed-token/program/src/lib.rs index 16fda9cb7e..e34bddaf0b 100644 --- a/programs/compressed-token/program/src/lib.rs +++ b/programs/compressed-token/program/src/lib.rs @@ -34,6 +34,9 @@ pub const LIGHT_CPI_SIGNER: CpiSigner = pub const MAX_ACCOUNTS: usize = 30; pub(crate) const MAX_PACKED_ACCOUNTS: usize = 40; +/// Maximum number of compression operations per instruction. +/// Used for compression_to_input lookup array sizing. +pub(crate) const MAX_COMPRESSIONS: usize = 32; // Custom ctoken instructions start at 100 to skip spl-token program instrutions. // When adding new instructions check anchor discriminators for collisions! @@ -78,11 +81,12 @@ pub enum InstructionType { /// 2. MintTo /// 3. UpdateMintAuthority /// 4. UpdateFreezeAuthority - /// 5. CreateSplMint - /// 6. MintToCToken - /// 7. UpdateMetadataField - /// 8. UpdateMetadataAuthority - /// 9. RemoveMetadataKey + /// 5. MintToCToken + /// 6. UpdateMetadataField + /// 7. UpdateMetadataAuthority + /// 8. RemoveMetadataKey + /// 9. DecompressMint + /// 10. CompressAndCloseCMint MintAction = 103, /// Claim rent for past completed epochs from compressible token account Claim = 104, diff --git a/programs/compressed-token/program/src/shared/mint_to_token_pool.rs b/programs/compressed-token/program/src/shared/mint_to_token_pool.rs index 42c2b80fd8..08cb00f873 100644 --- a/programs/compressed-token/program/src/shared/mint_to_token_pool.rs +++ b/programs/compressed-token/program/src/shared/mint_to_token_pool.rs @@ -10,7 +10,7 @@ use pinocchio::{ use crate::{shared::convert_program_error, LIGHT_CPI_SIGNER}; /// Mint tokens to the token pool using SPL token mint_to instruction. -/// This function is shared between create_spl_mint and mint_to_compressed processors +/// This function is used by mint_to_compressed processors /// to ensure consistent token pool management. #[profile] pub fn mint_to_token_pool( diff --git a/programs/compressed-token/program/src/shared/mod.rs b/programs/compressed-token/program/src/shared/mod.rs index c372db8f9f..3213704178 100644 --- a/programs/compressed-token/program/src/shared/mod.rs +++ b/programs/compressed-token/program/src/shared/mod.rs @@ -14,7 +14,9 @@ pub mod transfer_lamports; pub mod validate_ata_derivation; pub use config_account::{next_config_account, parse_config_account}; -pub use convert_program_error::{convert_pinocchio_token_error, convert_program_error, convert_token_error}; +pub use convert_program_error::{ + convert_pinocchio_token_error, convert_program_error, convert_token_error, +}; pub use create_pda_account::{create_pda_account, verify_pda}; pub use initialize_ctoken_account::create_compressible_account; pub use light_account_checks::AccountIterator; diff --git a/programs/compressed-token/program/src/shared/token_input.rs b/programs/compressed-token/program/src/shared/token_input.rs index fb704cda37..822bc1b241 100644 --- a/programs/compressed-token/program/src/shared/token_input.rs +++ b/programs/compressed-token/program/src/shared/token_input.rs @@ -97,7 +97,6 @@ pub fn set_input_compressed_account<'a>( owner_account }; - // TODO: allow freeze authority to decompress if has CompressOnlyExtension verify_owner_or_delegate_signer( signer_account, delegate_account, diff --git a/programs/compressed-token/program/tests/check_extensions.rs b/programs/compressed-token/program/tests/check_extensions.rs index d588950f76..70a17314e2 100644 --- a/programs/compressed-token/program/tests/check_extensions.rs +++ b/programs/compressed-token/program/tests/check_extensions.rs @@ -8,8 +8,7 @@ //! - Category 5: Direct check_mint_extensions tests use anchor_compressed_token::ErrorCode; -use anchor_lang::prelude::ProgramError; -use anchor_lang::solana_program::pubkey::Pubkey as SolanaPubkey; +use anchor_lang::{prelude::ProgramError, solana_program::pubkey::Pubkey as SolanaPubkey}; use light_account_checks::{ account_info::test_account_info::pinocchio::get_account_info, packed_accounts::ProgramPackedAccounts, @@ -21,7 +20,7 @@ use light_compressed_token::{ use light_ctoken_interface::instructions::{ extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData}, transfer2::{ - Compression, CompressionMode, CompressedTokenInstructionDataTransfer2, + CompressedTokenInstructionDataTransfer2, Compression, CompressionMode, MultiInputTokenDataWithContext, MultiTokenTransferOutputData, }, }; @@ -96,11 +95,8 @@ fn create_mock_t22_mint(config: &MintConfig) -> Vec { // Initialize extensions if config.has_pausable { - let ext = mint_state - .init_extension::(true) - .unwrap(); - ext.authority = - OptionalNonZeroPubkey::try_from(Some(SolanaPubkey::new_unique())).unwrap(); + let ext = mint_state.init_extension::(true).unwrap(); + ext.authority = OptionalNonZeroPubkey::try_from(Some(SolanaPubkey::new_unique())).unwrap(); ext.paused = PodBool::from(config.is_paused); } @@ -132,14 +128,11 @@ fn create_mock_t22_mint(config: &MintConfig) -> Vec { let ext = mint_state .init_extension::(true) .unwrap(); - ext.delegate = - OptionalNonZeroPubkey::try_from(Some(SolanaPubkey::new_unique())).unwrap(); + ext.delegate = OptionalNonZeroPubkey::try_from(Some(SolanaPubkey::new_unique())).unwrap(); } if config.has_metadata_pointer { - let ext = mint_state - .init_extension::(true) - .unwrap(); + let ext = mint_state.init_extension::(true).unwrap(); ext.metadata_address = OptionalNonZeroPubkey::try_from(Some(SolanaPubkey::new_unique())).unwrap(); } @@ -265,7 +258,9 @@ fn run_build_cache_test( ); let accounts = [mint_account]; - let packed_accounts = ProgramPackedAccounts { accounts: &accounts }; + let packed_accounts = ProgramPackedAccounts { + accounts: &accounts, + }; build_mint_extension_cache(&inputs, &packed_accounts).map(|_| ()) } @@ -426,7 +421,10 @@ fn test_compress_and_close_missing_compressed_only_fails() { }); let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); - assert_error(result, ErrorCode::CompressAndCloseMissingCompressedOnlyExtension); + assert_error( + result, + ErrorCode::CompressAndCloseMissingCompressedOnlyExtension, + ); } #[test] @@ -445,7 +443,10 @@ fn test_compress_and_close_empty_tlv_fails() { }); let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); - assert_error(result, ErrorCode::CompressAndCloseMissingCompressedOnlyExtension); + assert_error( + result, + ErrorCode::CompressAndCloseMissingCompressedOnlyExtension, + ); } // ============================================================================ @@ -466,7 +467,11 @@ fn test_input_with_restricted_no_outputs_succeeds() { }); let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); - assert!(result.is_ok(), "Should succeed with bypass, got {:?}", result); + assert!( + result.is_ok(), + "Should succeed with bypass, got {:?}", + result + ); } #[test] @@ -486,7 +491,11 @@ fn test_compress_and_close_with_compressed_only_succeeds() { }); let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); - assert!(result.is_ok(), "Should succeed with CompressedOnly, got {:?}", result); + assert!( + result.is_ok(), + "Should succeed with CompressedOnly, got {:?}", + result + ); } #[test] @@ -504,7 +513,11 @@ fn test_decompress_no_outputs_succeeds() { }); let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); - assert!(result.is_ok(), "Should succeed with bypass, got {:?}", result); + assert!( + result.is_ok(), + "Should succeed with bypass, got {:?}", + result + ); } #[test] @@ -522,7 +535,11 @@ fn test_compress_no_outputs_succeeds() { }); let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); - assert!(result.is_ok(), "Should succeed with bypass, got {:?}", result); + assert!( + result.is_ok(), + "Should succeed with bypass, got {:?}", + result + ); } // ============================================================================ @@ -555,7 +572,11 @@ fn test_t22_mint_no_extensions_succeeds() { }); let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); - assert!(result.is_ok(), "T22 without extensions should succeed, got {:?}", result); + assert!( + result.is_ok(), + "T22 without extensions should succeed, got {:?}", + result + ); } #[test] @@ -572,7 +593,11 @@ fn test_t22_mint_with_metadata_only_succeeds() { }); let result = run_build_cache_test(&inputs, &mint_data, SPL_TOKEN_2022_ID); - assert!(result.is_ok(), "MetadataPointer should succeed, got {:?}", result); + assert!( + result.is_ok(), + "MetadataPointer should succeed, got {:?}", + result + ); } // ============================================================================ @@ -618,7 +643,10 @@ fn test_check_mint_extensions_non_zero_fee() { ); let result = check_mint_extensions(&mint_account, false); - assert_error(result.map(|_| ()), ErrorCode::NonZeroTransferFeeNotSupported); + assert_error( + result.map(|_| ()), + ErrorCode::NonZeroTransferFeeNotSupported, + ); } #[test] @@ -679,7 +707,11 @@ fn test_check_mint_extensions_deny_restricted_non_restricted_succeeds() { // deny_restricted=true should succeed with non-restricted mint let result = check_mint_extensions(&mint_account, true); - assert!(result.is_ok(), "Non-restricted mint should succeed, got {:?}", result); + assert!( + result.is_ok(), + "Non-restricted mint should succeed, got {:?}", + result + ); } #[test] @@ -704,5 +736,9 @@ fn test_check_mint_extensions_valid_mint_succeeds() { // deny_restricted=false with all valid states should succeed let result = check_mint_extensions(&mint_account, false); - assert!(result.is_ok(), "Valid mint should succeed, got {:?}", result); + assert!( + result.is_ok(), + "Valid mint should succeed, got {:?}", + result + ); } diff --git a/programs/compressed-token/program/tests/mint_action.rs b/programs/compressed-token/program/tests/mint_action.rs index 666b336c56..0e548bea9a 100644 --- a/programs/compressed-token/program/tests/mint_action.rs +++ b/programs/compressed-token/program/tests/mint_action.rs @@ -9,7 +9,7 @@ use light_ctoken_interface::{ instructions::{ extensions::{token_metadata::TokenMetadataInstructionData, ExtensionInstructionData}, mint_action::{ - Action, CompressedMintInstructionData, CpiContext, CreateMint, CreateSplMintAction, + Action, CompressedMintInstructionData, CpiContext, CreateMint, MintActionCompressedInstructionData, MintToCTokenAction, MintToCompressedAction, Recipient, RemoveMetadataKeyAction, UpdateAuthority, UpdateMetadataAuthorityAction, UpdateMetadataFieldAction, @@ -83,12 +83,6 @@ fn random_update_authority_action(rng: &mut StdRng) -> UpdateAuthority { } } -fn random_create_spl_mint_action(rng: &mut StdRng) -> CreateSplMintAction { - CreateSplMintAction { - mint_bump: rng.gen::(), - } -} - fn random_update_metadata_field_action(rng: &mut StdRng) -> UpdateMetadataFieldAction { UpdateMetadataFieldAction { extension_index: rng.gen_range(0..=2) as u8, @@ -114,15 +108,14 @@ fn random_remove_metadata_key_action(rng: &mut StdRng) -> RemoveMetadataKeyActio } fn random_action(rng: &mut StdRng) -> Action { - match rng.gen_range(0..8) { + match rng.gen_range(0..7) { 0 => Action::MintToCompressed(random_mint_to_action(rng)), 1 => Action::UpdateMintAuthority(random_update_authority_action(rng)), 2 => Action::UpdateFreezeAuthority(random_update_authority_action(rng)), - 3 => Action::CreateSplMint(random_create_spl_mint_action(rng)), - 4 => Action::MintToCToken(random_mint_to_decompressed_action(rng)), - 5 => Action::UpdateMetadataField(random_update_metadata_field_action(rng)), - 6 => Action::UpdateMetadataAuthority(random_update_metadata_authority_action(rng)), - 7 => Action::RemoveMetadataKey(random_remove_metadata_key_action(rng)), + 3 => Action::MintToCToken(random_mint_to_decompressed_action(rng)), + 4 => Action::UpdateMetadataField(random_update_metadata_field_action(rng)), + 5 => Action::UpdateMetadataAuthority(random_update_metadata_authority_action(rng)), + 6 => Action::RemoveMetadataKey(random_remove_metadata_key_action(rng)), _ => unreachable!(), } } @@ -231,17 +224,11 @@ fn compute_expected_config(data: &MintActionCompressedInstructionData) -> Accoun .iter() .any(|action| matches!(action, Action::MintToCompressed(_))); - // 4. create_spl_mint (for with_mint_signer only) - let create_spl_mint = data - .actions - .iter() - .any(|action| matches!(action, Action::CreateSplMint(_))); - - // 5. cmint_decompressed - only based on metadata flag (matches AccountsConfig::new) + // 4. cmint_decompressed - only based on metadata flag (matches AccountsConfig::new) let cmint_decompressed = data.mint.as_ref().unwrap().metadata.cmint_decompressed; - // 6. with_mint_signer - let with_mint_signer = data.create_mint.is_some() || create_spl_mint; + // 5. with_mint_signer + let with_mint_signer = data.create_mint.is_some(); // 7. create_mint let create_mint = data.create_mint.is_some(); @@ -346,12 +333,6 @@ fn check_if_config_should_error(instruction_data: &MintActionCompressedInstructi .iter() .any(|action| matches!(action, Action::MintToCToken(_))); - // Check for CreateSplMint actions - let create_spl_mint = instruction_data - .actions - .iter() - .any(|action| matches!(action, Action::CreateSplMint(_))); - // Check for MintToCompressed actions let has_mint_to_actions = instruction_data .actions @@ -368,9 +349,8 @@ fn check_if_config_should_error(instruction_data: &MintActionCompressedInstructi // Error conditions matching AccountsConfig::new: // 1. has_mint_to_ctoken (MintToCToken actions not allowed) - // 2. create_spl_mint (CreateSplMint actions not allowed) - // 3. cmint_decompressed && has_mint_to_actions (mint decompressed + MintToCompressed not allowed) - has_mint_to_ctoken || create_spl_mint || (cmint_decompressed && has_mint_to_actions) + // 2. cmint_decompressed && has_mint_to_actions (mint decompressed + MintToCompressed not allowed) + has_mint_to_ctoken || (cmint_decompressed && has_mint_to_actions) } else { false } diff --git a/sdk-libs/ctoken-sdk/src/compressed_token/v2/mint_action/account_metas.rs b/sdk-libs/ctoken-sdk/src/compressed_token/v2/mint_action/account_metas.rs index 16d54fc7fc..79d85b983a 100644 --- a/sdk-libs/ctoken-sdk/src/compressed_token/v2/mint_action/account_metas.rs +++ b/sdk-libs/ctoken-sdk/src/compressed_token/v2/mint_action/account_metas.rs @@ -116,7 +116,7 @@ impl MintActionMetaConfig { } /// Set the mint_signer account with signing required. - /// Use for create_mint and create_spl_mint actions. + /// Use for create_mint actions. pub fn with_mint_signer(mut self, mint_signer: Pubkey) -> Self { self.mint_signer = Some(mint_signer); self.mint_signer_must_sign = true; @@ -158,7 +158,7 @@ impl MintActionMetaConfig { // mint_signer is present when creating a new mint or decompressing if let Some(mint_signer) = self.mint_signer { - // mint_signer needs to sign for create_mint/create_spl_mint, not for decompress_mint + // mint_signer needs to sign for create_mint, not for decompress_mint metas.push(AccountMeta::new_readonly( mint_signer, self.mint_signer_must_sign, diff --git a/sdk-libs/token-client/src/actions/mint_action.rs b/sdk-libs/token-client/src/actions/mint_action.rs index 5e2f59127a..e08329d0ef 100644 --- a/sdk-libs/token-client/src/actions/mint_action.rs +++ b/sdk-libs/token-client/src/actions/mint_action.rs @@ -23,7 +23,7 @@ use crate::instructions::mint_action::{ /// * `params` - Parameters for the mint action /// * `authority` - Authority keypair for the mint operations /// * `payer` - Account that pays for the transaction -/// * `mint_signer` - Optional mint signer for CreateSplMint action +/// * `mint_signer` - Optional mint signer for create_mint or DecompressMint action pub async fn mint_action( rpc: &mut R, params: MintActionParams, @@ -49,7 +49,7 @@ pub async fn mint_action( signers.push(authority); } - // Add mint signer if needed for CreateSplMint + // Add mint signer if needed for create_mint or DecompressMint if let Some(signer) = mint_signer { if !signers.iter().any(|s| s.pubkey() == signer.pubkey()) { signers.push(signer); From ff1f49b7725d7819c9b74f22b74a2ae6534441d2 Mon Sep 17 00:00:00 2001 From: ananas Date: Wed, 7 Jan 2026 01:14:08 +0000 Subject: [PATCH 13/18] refactor: validate CompressibleConfig state and simplify mint_output - Add config state validation (active required) during account parsing - Change compressible_config field to store parsed CompressibleConfig - Remove redundant config parsing in decompress_mint action - Refactor mint_output into serialize_decompressed_mint and serialize_compressed_mint helpers - Use cmint_decompressed flag directly after process_actions --- .../compressed_token/mint_action/accounts.rs | 16 +- .../mint_action/actions/decompress_mint.rs | 7 +- .../mint_action/mint_output.rs | 207 +++++++++--------- 3 files changed, 116 insertions(+), 114 deletions(-) diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs index 61dfdde529..b4727d6c15 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/accounts.rs @@ -1,6 +1,7 @@ use anchor_compressed_token::ErrorCode; use anchor_lang::solana_program::program_error::ProgramError; use light_account_checks::packed_accounts::ProgramPackedAccounts; +use light_compressible::config::CompressibleConfig; use light_ctoken_interface::{ instructions::mint_action::{ZAction, ZMintActionCompressedInstructionData}, CMINT_ADDRESS_TREE, @@ -12,7 +13,7 @@ use spl_pod::solana_msg::msg; use crate::shared::{ accounts::{CpiContextLightSystemAccounts, LightSystemAccounts}, - AccountIterator, + next_config_account, AccountIterator, }; pub struct MintActionAccounts<'info> { @@ -41,8 +42,8 @@ pub struct MintActionAccounts<'info> { /// Required accounts to execute an instruction /// with or without cpi context. pub struct ExecutingAccounts<'info> { - /// CompressibleConfig account - required when creating CMint (always compressible). - pub compressible_config: Option<&'info AccountInfo>, + /// CompressibleConfig - parsed and validated (active state) when creating CMint. + pub compressible_config: Option<&'info CompressibleConfig>, /// CMint Solana account (decompressed compressed mint). /// Required for DecompressMint action and when syncing with existing CMint. pub cmint: Option<&'info AccountInfo>, @@ -99,9 +100,12 @@ impl<'info> MintActionAccounts<'info> { packed_accounts: ProgramPackedAccounts { accounts: &[] }, }) } else { - // Parse compressible config when creating or closing CMint - let compressible_config = - iter.next_option("compressible_config", config.needs_compressible_accounts())?; + // Parse and validate compressible config when creating or closing CMint + let compressible_config = if config.needs_compressible_accounts() { + Some(next_config_account(&mut iter)?) + } else { + None + }; // CMint account required if already decompressed (for sync) OR being decompressed/closed let cmint = iter.next_option_mut("cmint", config.needs_cmint_account())?; diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs index 2a3838b7bf..1d224a27cf 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs @@ -18,7 +18,6 @@ use crate::{ shared::{ convert_program_error, create_pda_account::{create_pda_account, verify_pda}, - parse_config_account, }, }; @@ -75,13 +74,11 @@ pub fn process_decompress_mint_action( .cmint .ok_or(ErrorCode::MintActionMissingCMintAccount)?; - // 3. Get and validate CompressibleConfig account - let config_account = executing + // 3. Get CompressibleConfig (already parsed and validated as active) + let config = executing .compressible_config .ok_or(ErrorCode::MissingCompressibleConfig)?; - let config = parse_config_account(config_account)?; - // 5. Validate write_top_up doesn't exceed max_top_up if action.write_top_up > config.rent_config.max_top_up as u32 { msg!( diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs index 1c417186e5..6e8d713052 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs @@ -44,103 +44,47 @@ pub fn process_output_compressed_account<'a>( &validated_accounts.packed_accounts, &mut compressed_mint, )?; - // When decompressed (CMint is source of truth), use zero values - // TODO: check whether I can just mutate is_decompressed instead of using has_compress_and_close_cmint_action - // TODO: double check that we cannot close and create in the same instruction - let cmint_is_decompressed = accounts_config.cmint_is_decompressed(); - // Serialize state into CMint solana account - // SKIP if CompressAndCloseCMint action is present (CMint is being closed) - // SKIP if DecompressMint action is present (CMint is being closed) - if cmint_is_decompressed { - let cmint_account = validated_accounts - .get_cmint() - .ok_or(ErrorCode::CMintNotFound)?; - if !accounts_config.has_compress_and_close_cmint_action { - let num_bytes = cmint_account.data_len() as u64; - let current_lamports = cmint_account.lamports(); - let rent_exemption = get_rent_exemption_lamports(num_bytes) - .map_err(|_| ErrorCode::CMintTopUpCalculationFailed)?; - - // Skip top-up calculation if decompress mint action is present. - if !accounts_config.has_decompress_mint_action { - // Handle top-up for compressed mint (compression info is now embedded directly) - // Get current slot for top-up calculation - let current_slot = Clock::get() - .map_err(|_| ProgramError::UnsupportedSysvar)? - .slot; - // Calculate top-up amount using embedded compression info - let top_up = compressed_mint - .compression - .calculate_top_up_lamports( - num_bytes, - current_slot, - current_lamports, - rent_exemption, - ) - .map_err(|_| ErrorCode::CMintTopUpCalculationFailed)?; - - if top_up > 0 { - let fee_payer = validated_accounts - .executing - .as_ref() - .map(|exec| exec.system.fee_payer) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - transfer_lamports(top_up, fee_payer, cmint_account) - .map_err(convert_program_error)?; - } - } - - let serialized = compressed_mint - .try_to_vec() - .map_err(|_| ErrorCode::MintActionOutputSerializationFailed)?; - let required_size = serialized.len(); - - // Resize if needed (e.g., metadata extensions added) - if cmint_account.data_len() < required_size { - cmint_account - .resize(required_size) - .map_err(|_| ErrorCode::CMintResizeFailed)?; - - // Transfer additional lamports for rent if resized - let rent = Rent::get().map_err(|_| ProgramError::UnsupportedSysvar)?; - let required_lamports = rent.minimum_balance(required_size); - if cmint_account.lamports() < required_lamports { - let fee_payer = validated_accounts - .executing - .as_ref() - .map(|exec| exec.system.fee_payer) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - transfer_lamports( - required_lamports - cmint_account.lamports(), - fee_payer, - cmint_account, - ) - .map_err(convert_program_error)?; - } - } - - let mut cmint_data = cmint_account - .try_borrow_mut_data() - .map_err(|_| ProgramError::AccountBorrowFailed)?; - if cmint_data.len() < serialized.len() { - msg!( - "CMint account too small: {} < {}", - cmint_data.len(), - serialized.len() - ); - return Err(ErrorCode::CMintResizeFailed.into()); - } - cmint_data[..serialized.len()].copy_from_slice(&serialized); - } + + if compressed_mint.metadata.cmint_decompressed { + serialize_decompressed_mint(validated_accounts, accounts_config, &mut compressed_mint)?; + } + + serialize_compressed_mint( + mint_account, + compressed_mint, + parsed_instruction_data, + queue_indices, + ) +} + +#[inline(always)] +fn split_mint_and_token_accounts<'a>( + output_compressed_accounts: &'a mut [ZOutputCompressedAccountWithPackedContextMut<'a>], +) -> ( + &'a mut ZOutputCompressedAccountWithPackedContextMut<'a>, + &'a mut [ZOutputCompressedAccountWithPackedContextMut<'a>], +) { + if output_compressed_accounts.len() == 1 { + (&mut output_compressed_accounts[0], &mut []) + } else { + let (mint_account, token_accounts) = output_compressed_accounts.split_at_mut(1); + (&mut mint_account[0], token_accounts) } +} +fn serialize_compressed_mint<'a>( + mint_account: &'a mut ZOutputCompressedAccountWithPackedContextMut<'a>, + compressed_mint: CompressedMint, + parsed_instruction_data: &ZMintActionCompressedInstructionData, + queue_indices: &QueueIndices, +) -> Result<(), ProgramError> { let compressed_account_data = mint_account .compressed_account .data .as_mut() .ok_or(ErrorCode::MintActionOutputSerializationFailed)?; - let (discriminator, data_hash) = if cmint_is_decompressed { + let (discriminator, data_hash) = if compressed_mint.metadata.cmint_decompressed { if !compressed_account_data.data.is_empty() { msg!( "Data allocation for output mint account is wrong: {} (expected) != {} ", @@ -170,13 +114,13 @@ pub fn process_output_compressed_account<'a>( compressed_account_data .data .copy_from_slice(data.as_slice()); + ( COMPRESSED_MINT_DISCRIMINATOR, Sha256BE::hash(compressed_account_data.data)?, ) }; - // Set mint output compressed account fields except the data. mint_account.set( crate::LIGHT_CPI_SIGNER.program_id.into(), 0, @@ -185,21 +129,78 @@ pub fn process_output_compressed_account<'a>( discriminator, data_hash, )?; - Ok(()) } -#[inline(always)] -fn split_mint_and_token_accounts<'a>( - output_compressed_accounts: &'a mut [ZOutputCompressedAccountWithPackedContextMut<'a>], -) -> ( - &'a mut ZOutputCompressedAccountWithPackedContextMut<'a>, - &'a mut [ZOutputCompressedAccountWithPackedContextMut<'a>], -) { - if output_compressed_accounts.len() == 1 { - (&mut output_compressed_accounts[0], &mut []) - } else { - let (mint_account, token_accounts) = output_compressed_accounts.split_at_mut(1); - (&mut mint_account[0], token_accounts) +fn serialize_decompressed_mint( + validated_accounts: &MintActionAccounts, + accounts_config: &AccountsConfig, + compressed_mint: &mut CompressedMint, +) -> Result<(), ProgramError> { + let cmint_account = validated_accounts + .get_cmint() + .ok_or(ErrorCode::CMintNotFound)?; + let num_bytes = cmint_account.data_len() as u64; + let current_lamports = cmint_account.lamports(); + let rent_exemption = get_rent_exemption_lamports(num_bytes) + .map_err(|_| ErrorCode::CMintTopUpCalculationFailed)?; + + // Skip top-up calculation if decompress mint action is present + // (rent was just paid during account creation). + if !accounts_config.has_decompress_mint_action { + // Handle top-up for compressed mint (compression info is now embedded directly) + // Get current slot for top-up calculation + let current_slot = Clock::get().map_err(convert_program_error)?.slot; + // Calculate top-up amount using embedded compression info + let top_up = compressed_mint + .compression + .calculate_top_up_lamports(num_bytes, current_slot, current_lamports, rent_exemption) + .map_err(|_| ErrorCode::CMintTopUpCalculationFailed)?; + + if top_up > 0 { + let fee_payer = validated_accounts + .executing + .as_ref() + .map(|exec| exec.system.fee_payer) + .ok_or(ProgramError::NotEnoughAccountKeys)?; + transfer_lamports(top_up, fee_payer, cmint_account).map_err(convert_program_error)?; + } } + + let serialized = compressed_mint + .try_to_vec() + .map_err(|_| ErrorCode::MintActionOutputSerializationFailed)?; + let required_size = serialized.len(); + + // Resize if needed (e.g., metadata extensions added) + if cmint_account.data_len() < required_size { + cmint_account + .resize(required_size) + .map_err(|_| ErrorCode::CMintResizeFailed)?; + + // Transfer additional lamports for rent if resized + let rent = Rent::get().map_err(|_| ProgramError::UnsupportedSysvar)?; + let required_lamports = rent.minimum_balance(required_size); + if cmint_account.lamports() < required_lamports { + let fee_payer = validated_accounts + .executing + .as_ref() + .map(|exec| exec.system.fee_payer) + .ok_or(ProgramError::NotEnoughAccountKeys)?; + transfer_lamports( + required_lamports - cmint_account.lamports(), + fee_payer, + cmint_account, + ) + .map_err(convert_program_error)?; + } + } + + let mut cmint_data = cmint_account + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + // SAFETY: we previously resized the account if needed + cmint_data[..serialized.len()].copy_from_slice(&serialized); + + Ok(()) } From 4abf946a14d0bbe27c82556bf5e3d5c4f48aa208 Mon Sep 17 00:00:00 2001 From: ananas Date: Wed, 7 Jan 2026 01:38:38 +0000 Subject: [PATCH 14/18] fix comments --- .../actions/compress_and_close_cmint.rs | 3 +-- .../mint_action/actions/decompress_mint.rs | 27 ++++++++++--------- .../mint_action/actions/mint_to.rs | 11 ++------ .../mint_action/actions/mint_to_ctoken.rs | 3 +-- .../mint_action/actions/update_metadata.rs | 2 +- .../mint_action/mint_input.rs | 1 - .../mint_action/mint_output.rs | 7 +---- .../compressed_token/mint_action/processor.rs | 2 +- .../mint_action/zero_copy_config.rs | 2 +- .../compressed_token/transfer2/accounts.rs | 4 +-- .../transfer2/change_account.rs | 2 +- .../ctoken/compress_or_decompress_ctokens.rs | 4 +-- .../src/compressed_token/transfer2/cpi.rs | 3 +-- .../compressed_token/transfer2/processor.rs | 11 ++++---- .../transfer2/token_outputs.rs | 2 +- .../program/src/compressible/claim.rs | 2 +- .../src/compressible/withdraw_funding_pool.rs | 4 +-- .../program/src/ctoken/close/processor.rs | 2 +- .../program/src/ctoken/transfer/shared.rs | 4 +-- .../program/src/extensions/processor.rs | 1 - programs/compressed-token/program/src/lib.rs | 2 +- .../program/src/shared/accounts.rs | 12 ++++----- .../program/src/shared/compressible_top_up.rs | 3 +-- .../program/src/shared/cpi.rs | 2 +- .../src/shared/initialize_ctoken_account.rs | 2 +- .../program/src/shared/owner_validation.rs | 2 +- 26 files changed, 51 insertions(+), 69 deletions(-) diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs index 46f6249db2..d2d1a1004a 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/compress_and_close_cmint.rs @@ -71,10 +71,9 @@ pub fn process_compress_and_close_cmint_action( return Err(ErrorCode::InvalidCMintAccount.into()); } - // 4. Access compression info directly (all cmints now have embedded compression) let compression_info = &compressed_mint.compression; - // 5. Verify rent_sponsor matches compression info + // 4. Verify rent_sponsor matches compression info if !pubkey_eq(rent_sponsor.key(), &compression_info.rent_sponsor) { msg!("Rent sponsor does not match compression info"); return Err(ErrorCode::InvalidRentSponsor.into()); diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs index 1d224a27cf..06c363aff0 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs @@ -59,7 +59,7 @@ pub fn process_decompress_mint_action( return Err(ErrorCode::CMintAlreadyExists.into()); } - // rent_payment == 1 is rejected - epoch boundary edge case + // 2. rent_payment == 1 is rejected - epoch boundary edge case if action.rent_payment == 1 { msg!("Prefunding for exactly 1 epoch is not allowed. Use 0 or 2+ epochs."); return Err(ErrorCode::OneEpochPrefundingNotAllowed.into()); @@ -79,7 +79,7 @@ pub fn process_decompress_mint_action( .compressible_config .ok_or(ErrorCode::MissingCompressibleConfig)?; - // 5. Validate write_top_up doesn't exceed max_top_up + // 4. Validate write_top_up doesn't exceed max_top_up if action.write_top_up > config.rent_config.max_top_up as u32 { msg!( "write_top_up {} exceeds max_top_up {}", @@ -89,7 +89,7 @@ pub fn process_decompress_mint_action( return Err(ErrorCode::WriteTopUpExceedsMaximum.into()); } - // 6. Get rent_sponsor and verify it matches config + // Get rent_sponsor and verify it matches config let rent_sponsor = executing .rent_sponsor .ok_or(ErrorCode::MissingRentSponsor)?; @@ -99,12 +99,12 @@ pub fn process_decompress_mint_action( return Err(ErrorCode::InvalidRentSponsor.into()); } - // 7. Get current slot for last_claimed_slot + // Get current slot for last_claimed_slot let current_slot = Clock::get() .map_err(|_| ProgramError::UnsupportedSysvar)? .slot; - // 8. Set compression info directly on compressed_mint (all cmints now have embedded compression) + // 5. Set compression info on compressed_mint compressed_mint.compression = CompressionInfo { config_account_version: config.version, compress_to_pubkey: 0, // Not applicable for CMint @@ -122,7 +122,7 @@ pub fn process_decompress_mint_action( }, }; - // 9. Verify PDA derivation + // 6. Verify PDA derivation let seeds: [&[u8]; 2] = [COMPRESSED_MINT_SEED, mint_signer.key()]; verify_pda( cmint.key(), @@ -131,17 +131,18 @@ pub fn process_decompress_mint_action( &crate::LIGHT_CPI_SIGNER.program_id, )?; - // 10. Calculate account size AFTER adding extension (using borsh serialization) + // 7. Account creation: rent_sponsor pays rent exemption, fee_payer pays Light rent + // 7a. Calculate account size AFTER adding extension (using borsh serialization) let account_size = borsh::to_vec(compressed_mint) .map_err(|_| ErrorCode::MintActionOutputSerializationFailed)? .len(); - // 11. Calculate Light Protocol rent (base_rent + bytes * lamports_per_byte * epochs + compression_cost) + // 7b. Calculate Light Protocol rent (base_rent + bytes * lamports_per_byte * epochs + compression_cost) let light_rent = config .rent_config .get_rent_with_compression_cost(account_size as u64, action.rent_payment as u64); - // 12. Build seeds for rent_sponsor PDA (to sign the transfer) + // 7c. Build seeds for rent_sponsor PDA (to sign the transfer) let version_bytes = config.version.to_le_bytes(); let rent_sponsor_bump_bytes = [config.rent_sponsor_bump]; let rent_sponsor_seeds = [ @@ -150,7 +151,7 @@ pub fn process_decompress_mint_action( Seed::from(rent_sponsor_bump_bytes.as_ref()), ]; - // 13. Build seeds for CMint PDA + // 7d. Build seeds for CMint PDA let cmint_bump_bytes = [action.cmint_bump]; let cmint_seeds = [ Seed::from(COMPRESSED_MINT_SEED), @@ -158,7 +159,7 @@ pub fn process_decompress_mint_action( Seed::from(cmint_bump_bytes.as_ref()), ]; - // 14. Create CMint PDA account + // 7e. Create CMint PDA account // rent_sponsor pays ONLY the rent exemption (minimum_balance) // additional_lamports = None means create_pda_account only pays rent exemption create_pda_account( @@ -170,7 +171,7 @@ pub fn process_decompress_mint_action( None, // rent_sponsor pays ONLY rent exemption )?; - // 15. fee_payer pays the Light Protocol rent + // 7f. fee_payer pays the Light Protocol rent Transfer { from: fee_payer, to: cmint, @@ -179,7 +180,7 @@ pub fn process_decompress_mint_action( .invoke() .map_err(convert_program_error)?; - // 16. Set the cmint_decompressed flag + // 8. Set the cmint_decompressed flag compressed_mint.metadata.cmint_decompressed = true; Ok(()) diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to.rs index 9ec58c163c..1f096e686c 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to.rs @@ -18,15 +18,8 @@ use crate::{ /// ## Process Steps /// 1. **Authority Validation**: Verify signer matches current mint authority from compressed mint state /// 2. **Amount Calculation**: Sum recipient amounts with overflow protection -/// 3. **Lamports Calculation**: Calculate total lamports for compressed accounts (if specified) -/// 4. **Supply Update**: Calculate new total supply with overflow protection -/// 5. **SPL Mint Synchronization**: For initialized SPL mints, validate accounts and mint equivalent tokens to token pool via CPI -/// 6. **Compressed Account Creation**: Create new compressed token account for each recipient -/// -/// ## SPL Mint Synchronization -/// When `compressed_mint.metadata.cmint_decompressed` is true and an SPL mint exists for this compressed mint, -/// the function maintains consistency between the compressed token supply and the underlying SPL mint supply -/// by minting equivalent tokens to a program-controlled token pool account via CPI to SPL Token 2022. +/// 3. **Supply Update**: Calculate new total supply with overflow protection +/// 4. **Compressed Account Creation**: Create new compressed token account for each recipient #[allow(clippy::too_many_arguments)] #[profile] pub fn process_mint_to_compressed_action<'a>( diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to_ctoken.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to_ctoken.rs index 03ce5c21eb..42c16fd356 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to_ctoken.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/mint_to_ctoken.rs @@ -41,8 +41,7 @@ pub fn process_mint_to_ctoken_action( let token_account_info = packed_accounts.get_u8(action.account_index, "ctoken mint to recipient")?; - // Authority check now performed above - safe to proceed with decompression - // Use the mint_ctokens constructor for simple decompression operations + // Authority check performed above - proceed with minting to CToken account let inputs = CTokenCompressionInputs::mint_ctokens( amount, mint.to_bytes(), diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/actions/update_metadata.rs b/programs/compressed-token/program/src/compressed_token/mint_action/actions/update_metadata.rs index d15bd9d840..7738b17be2 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/actions/update_metadata.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/actions/update_metadata.rs @@ -122,7 +122,7 @@ pub fn process_update_metadata_authority_action( Ok(()) } -/// Only checks authority, the key is removed during data allocation. +/// Removes a metadata key from additional_metadata after authority check. #[profile] pub fn process_remove_metadata_key_action( action: &ZRemoveMetadataKeyAction, diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs index 80af25aac1..0f9f8a23a1 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/mint_input.rs @@ -40,7 +40,6 @@ pub fn create_input_compressed_mint_account( .mint .as_ref() .ok_or(ProgramError::InvalidInstructionData)?; - // Return it so that we dont deserialize it twice. let compressed_mint = CompressedMint::try_from(mint_data)?; let bytes = compressed_mint .try_to_vec() diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs index 6e8d713052..9831c3bc3c 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs @@ -22,7 +22,7 @@ use crate::{ shared::{convert_program_error, transfer_lamports::transfer_lamports}, }; -/// Processes the output compressed mint account and returns the modified mint for CMint sync. +/// Processes the output compressed mint account, syncing to CMint if decompressed. #[profile] pub fn process_output_compressed_account<'a>( parsed_instruction_data: &ZMintActionCompressedInstructionData, @@ -97,7 +97,6 @@ fn serialize_compressed_mint<'a>( // of a closed compressed account without any data. ([0u8; 8], [0u8; 32]) } else { - // Serialize compressed mint for compressed account let data = compressed_mint .try_to_vec() .map_err(|e| ProgramError::BorshIoError(e.to_string()))?; @@ -110,7 +109,6 @@ fn serialize_compressed_mint<'a>( return Err(ProgramError::InvalidAccountData); } - // Copy data and compute hash compressed_account_data .data .copy_from_slice(data.as_slice()); @@ -148,10 +146,7 @@ fn serialize_decompressed_mint( // Skip top-up calculation if decompress mint action is present // (rent was just paid during account creation). if !accounts_config.has_decompress_mint_action { - // Handle top-up for compressed mint (compression info is now embedded directly) - // Get current slot for top-up calculation let current_slot = Clock::get().map_err(convert_program_error)?.slot; - // Calculate top-up amount using embedded compression info let top_up = compressed_mint .compression .calculate_top_up_lamports(num_bytes, current_slot, current_lamports, rent_exemption) diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs index cb4f5e4466..5ba9cdf883 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/processor.rs @@ -150,7 +150,7 @@ pub fn process_mint_action( false, // no sol_pool_pda None, executing.system.cpi_context.map(|x| *x.key()), - false, // write to cpi context account + false, // don't write to cpi context account ) } else { if validated_accounts.write_to_cpi_context_system.is_none() { diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs b/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs index ecef2e7ebb..9ed12f3e53 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/zero_copy_config.rs @@ -47,7 +47,7 @@ pub fn get_zero_copy_configs( ZAction::UpdateFreezeAuthority(_) => {} ZAction::RemoveMetadataKey(_) => {} ZAction::UpdateMetadataAuthority(auth_action) => { - // Update output config for authority revocation + // Validate extension index for authority revocation if auth_action.new_authority.to_bytes() == [0u8; 32] { let extension_index = auth_action.extension_index as usize; if extension_index >= output_extensions_config.len() { diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs b/programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs index 7a46e5bf5b..1eb0ecd67c 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/accounts.rs @@ -105,7 +105,7 @@ impl<'info> Transfer2Accounts<'info> { } /// Extract CPI accounts slice for light-system-program invocation - /// Includes static accounts + tree accounts based on highest tree index + /// Includes static accounts + tree accounts identified by owner /// Returns (cpi_accounts_slice, tree_accounts) #[profile] #[inline(always)] @@ -137,7 +137,7 @@ impl<'info> Transfer2Accounts<'info> { } } -/// Extract tree accounts by finding the highest tree index and using it as closing offset +/// Extract tree accounts by checking account owner matches account-compression program #[profile] #[inline(always)] pub fn extract_tree_accounts<'info>( diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/change_account.rs b/programs/compressed-token/program/src/compressed_token/transfer2/change_account.rs index 3b35522c4d..e65ea661bb 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/change_account.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/change_account.rs @@ -1,4 +1,4 @@ -//! unused +//! Change account handling for lamports differences in Transfer2 use anchor_compressed_token::ErrorCode; use anchor_lang::prelude::ProgramError; use light_account_checks::packed_accounts::ProgramPackedAccounts; diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs index 1c5f61f478..2270a717d4 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_or_decompress_ctokens.rs @@ -84,8 +84,8 @@ pub fn compress_or_decompress_ctokens( ZCompressionMode::Decompress => { apply_decompress_extension_state(&mut ctoken, token_account_info, decompress_inputs)?; - // Decompress: add to solana account - // Update the balance in the compressed token account + // Decompress: add to CToken account + // Update the balance in the CToken solana account ctoken.base.amount.set( current_balance .checked_add(amount) diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/cpi.rs b/programs/compressed-token/program/src/compressed_token/transfer2/cpi.rs index aa06a1671d..98498171f0 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/cpi.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/cpi.rs @@ -67,10 +67,9 @@ pub fn allocate_cpi_bytes( output_accounts.push((false, data_len)); // Token accounts don't have addresses } - // Add extra output account for change account if needed (no delegate, no token data) + // Add extra output account for change account if needed (no delegate) if inputs.with_lamports_change_account_merkle_tree_index != 0 { output_accounts.push((false, compressed_token_data_len(false))); - // No delegate } let mut input_accounts = ArrayVec::new(); diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/processor.rs b/programs/compressed-token/program/src/compressed_token/transfer2/processor.rs index 8e7597b029..8c9b2800bc 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/processor.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/processor.rs @@ -37,12 +37,11 @@ use crate::{ /// 1. Unpack compressed input accounts and input token data, this uses /// standardized signer / delegate and will fail in proof verification in /// case either is invalid. -/// 2. Check that compressed accounts are of same mint. -/// 3. Check that sum of input compressed accounts is equal to sum of output -/// compressed accounts -/// 4. create_output_compressed_accounts -/// 5. Serialize and add token_data data to in compressed_accounts. -/// 6. Invoke light_system_program::execute_compressed_transaction. +/// 2. Check that sum of input compressed accounts equals sum of output +/// compressed accounts (supports multi-mint) +/// 3. create_output_compressed_accounts +/// 4. Serialize and add token_data data to in compressed_accounts. +/// 5. Invoke light_system_program::execute_compressed_transaction. #[profile] pub fn process_transfer2( accounts: &[AccountInfo], diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/token_outputs.rs b/programs/compressed-token/program/src/compressed_token/transfer2/token_outputs.rs index 17d65942bf..ab6b2ec6d0 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/token_outputs.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/token_outputs.rs @@ -13,7 +13,7 @@ use pinocchio::account_info::AccountInfo; use super::check_extensions::validate_tlv_and_get_frozen; use crate::shared::token_output::set_output_compressed_account; -/// Process output compressed accounts and return total output lamports +/// Process output compressed accounts #[profile] #[inline(always)] pub fn set_output_compressed_accounts<'a>( diff --git a/programs/compressed-token/program/src/compressible/claim.rs b/programs/compressed-token/program/src/compressible/claim.rs index 1518884ff5..5bfed2337b 100644 --- a/programs/compressed-token/program/src/compressible/claim.rs +++ b/programs/compressed-token/program/src/compressible/claim.rs @@ -39,7 +39,7 @@ impl<'a> ClaimAccounts<'a> { .map_err(ProgramError::from)?; if *config_account.compression_authority.as_array() != *compression_authority.key() { - msg!("invalid rent authority"); + msg!("invalid compression authority"); return Err(ErrorCode::InvalidCompressAuthority.into()); } if *config_account.rent_sponsor.as_array() != *rent_sponsor.key() { diff --git a/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs b/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs index 0a54c94eb9..c4be07dd83 100644 --- a/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs +++ b/programs/compressed-token/program/src/compressible/withdraw_funding_pool.rs @@ -71,7 +71,7 @@ pub fn process_withdraw_funding_pool( account_infos: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), ProgramError> { - // Parse instruction data: [bump: u8][amount: u64] + // Parse instruction data: [amount: u64] if instruction_data.len() < 8 { msg!("Invalid instruction data length"); return Err(ProgramError::InvalidInstructionData); @@ -98,7 +98,7 @@ pub fn process_withdraw_funding_pool( return Err(ProgramError::InsufficientFunds); } - // Prepare seeds for invoke_signed - the pool PDA is derived from [b"pool", compression_authority] + // Prepare seeds for invoke_signed - rent_sponsor PDA is derived from [b"rent_sponsor", version, bump] let bump_bytes = [rent_sponsor_bump]; let seed_array = [ Seed::from(b"rent_sponsor".as_slice()), diff --git a/programs/compressed-token/program/src/ctoken/close/processor.rs b/programs/compressed-token/program/src/ctoken/close/processor.rs index 3ce04f9a16..e9c36c9fd3 100644 --- a/programs/compressed-token/program/src/ctoken/close/processor.rs +++ b/programs/compressed-token/program/src/ctoken/close/processor.rs @@ -32,7 +32,7 @@ pub fn process_close_token_account( } /// Validates that a ctoken solana account is ready to be closed. -/// The rent authority cannot close the account. +/// Only the owner or close_authority can close the account. #[profile] pub fn validate_token_account_close_instruction( accounts: &CloseTokenAccountAccounts, diff --git a/programs/compressed-token/program/src/ctoken/transfer/shared.rs b/programs/compressed-token/program/src/ctoken/transfer/shared.rs index 3b2ec322fb..8b1d4f5819 100644 --- a/programs/compressed-token/program/src/ctoken/transfer/shared.rs +++ b/programs/compressed-token/program/src/ctoken/transfer/shared.rs @@ -51,7 +51,7 @@ pub struct TransferAccounts<'a> { } /// Process transfer extensions for CTokenTransfer instruction. -/// Restricted extensions are NOT allowed (and will fail anyway due to missing mint). +/// Restricted extensions are NOT allowed (requires mint account which is not provided). #[inline(always)] #[profile] pub fn process_transfer_extensions_transfer( @@ -78,7 +78,7 @@ pub fn process_transfer_extensions_transfer_checked( /// /// # Arguments /// * `transfer_accounts` - Account references for source, destination, authority, and optional mint -/// * `max_top_up` - Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (0 = no limit) +/// * `max_top_up` - Maximum lamports for top-up. Transaction fails if exceeded. (0 = no limit) /// * `deny_restricted_extensions` - If true, reject source accounts with restricted T22 extensions /// /// Returns: diff --git a/programs/compressed-token/program/src/extensions/processor.rs b/programs/compressed-token/program/src/extensions/processor.rs index 2083ef34a6..097c26e0d1 100644 --- a/programs/compressed-token/program/src/extensions/processor.rs +++ b/programs/compressed-token/program/src/extensions/processor.rs @@ -8,7 +8,6 @@ use light_program_profiler::profile; use crate::extensions::token_metadata::create_output_token_metadata; /// Set extensions state in output compressed account. -/// Compute extensions hash chain. #[inline(always)] #[profile] pub fn extensions_state_in_output_compressed_account( diff --git a/programs/compressed-token/program/src/lib.rs b/programs/compressed-token/program/src/lib.rs index e34bddaf0b..232e0b9a6c 100644 --- a/programs/compressed-token/program/src/lib.rs +++ b/programs/compressed-token/program/src/lib.rs @@ -38,7 +38,7 @@ pub(crate) const MAX_PACKED_ACCOUNTS: usize = 40; /// Used for compression_to_input lookup array sizing. pub(crate) const MAX_COMPRESSIONS: usize = 32; -// Custom ctoken instructions start at 100 to skip spl-token program instrutions. +// Instruction discriminators use SPL Token values (3-18) for compatibility plus custom values (100+). // When adding new instructions check anchor discriminators for collisions! #[repr(u8)] pub enum InstructionType { diff --git a/programs/compressed-token/program/src/shared/accounts.rs b/programs/compressed-token/program/src/shared/accounts.rs index 14e2e2a8eb..4e4cb9334c 100644 --- a/programs/compressed-token/program/src/shared/accounts.rs +++ b/programs/compressed-token/program/src/shared/accounts.rs @@ -33,17 +33,17 @@ pub struct LightSystemAccounts<'info> { pub cpi_authority_pda: &'info AccountInfo, /// Registered program PDA (index 2) - non-mutable pub registered_program_pda: &'info AccountInfo, - /// Account compression authority (index 4) - non-mutable + /// Account compression authority (index 3) - non-mutable pub account_compression_authority: &'info AccountInfo, - /// Account compression program (index 5) - non-mutable + /// Account compression program (index 4) - non-mutable pub account_compression_program: &'info AccountInfo, - /// System program (index 9) - non-mutable + /// System program (index 5) - non-mutable pub system_program: &'info AccountInfo, - /// Sol pool PDA (index 7) - optional, mutable if present + /// Sol pool PDA (index 6) - optional, mutable if present pub sol_pool_pda: Option<&'info AccountInfo>, - /// SOL decompression recipient (index 8) - optional, mutable, for SOL decompression + /// SOL decompression recipient (index 7) - optional, mutable, for SOL decompression pub sol_decompression_recipient: Option<&'info AccountInfo>, - /// CPI context account (index 10) - optional, non-mutable + /// CPI context account (index 8) - optional, mutable pub cpi_context: Option<&'info AccountInfo>, } diff --git a/programs/compressed-token/program/src/shared/compressible_top_up.rs b/programs/compressed-token/program/src/shared/compressible_top_up.rs index 3a1f732732..bd57477bb0 100644 --- a/programs/compressed-token/program/src/shared/compressible_top_up.rs +++ b/programs/compressed-token/program/src/shared/compressible_top_up.rs @@ -15,7 +15,7 @@ use super::{ }; /// Calculate and execute top-up transfers for compressible CMint and CToken accounts. -/// Both accounts are optional - if an account doesn't have compressible extension, it's skipped. +/// CMint always has compression info. CToken requires Compressible extension or errors. /// /// # Arguments /// * `cmint` - The CMint account (may or may not have Compressible extension) @@ -99,7 +99,6 @@ pub fn calculate_and_execute_compressible_top_ups<'a>( } /// Process compression top-up using embedded compression info. -/// All ctoken accounts now have compression info embedded directly in meta. #[inline(always)] pub fn process_compression_top_up( compression: &T, diff --git a/programs/compressed-token/program/src/shared/cpi.rs b/programs/compressed-token/program/src/shared/cpi.rs index e7e407a463..d1efec5576 100644 --- a/programs/compressed-token/program/src/shared/cpi.rs +++ b/programs/compressed-token/program/src/shared/cpi.rs @@ -130,7 +130,7 @@ pub fn execute_cpi_invoke( Ok(()) } -/// Eqivalent to pinocchio::cpi::slice_invoke_signed except: +/// Equivalent to pinocchio::cpi::slice_invoke_signed except: /// 1. account_infos: &[&AccountInfo] -> &[AccountInfo] /// 2. Error prints #[inline] diff --git a/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs b/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs index 162a6fd755..790d4996b4 100644 --- a/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs +++ b/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs @@ -197,7 +197,7 @@ pub fn initialize_ctoken_account( let mut token_account_data = AccountInfoTrait::try_borrow_mut_data(token_account_info)?; // Use new_zero_copy to initialize the token account - // This sets mint, owner, state, compression_only, account_type, and extensions + // This sets mint, owner, state, account_type, and extensions let (mut ctoken, _) = CToken::new_zero_copy(&mut token_account_data, zc_config).map_err(|e| { msg!("Failed to initialize CToken: {:?}", e); diff --git a/programs/compressed-token/program/src/shared/owner_validation.rs b/programs/compressed-token/program/src/shared/owner_validation.rs index ea0e5a42e1..2903f8a9f1 100644 --- a/programs/compressed-token/program/src/shared/owner_validation.rs +++ b/programs/compressed-token/program/src/shared/owner_validation.rs @@ -108,6 +108,6 @@ pub fn check_ctoken_owner( } } - // Authority is neither owner, account delegate, nor permanent delegate + // Authority is neither owner nor permanent delegate Err(ErrorCode::OwnerMismatch.into()) } From 4e4a7098f96ea8a4372fc91cc6002137068324d2 Mon Sep 17 00:00:00 2001 From: ananas Date: Wed, 7 Jan 2026 01:44:56 +0000 Subject: [PATCH 15/18] fix feedback --- programs/compressed-token/anchor/src/lib.rs | 2 ++ programs/compressed-token/program/docs/ctoken/MINT_TO.md | 2 +- .../program/src/compressed_token/mint_action/mint_output.rs | 4 ++-- programs/compressed-token/program/src/ctoken/create.rs | 5 +++-- programs/compressed-token/program/src/lib.rs | 6 +++--- .../program/src/shared/initialize_ctoken_account.rs | 3 +-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/programs/compressed-token/anchor/src/lib.rs b/programs/compressed-token/anchor/src/lib.rs index d74e90eb77..6132f0cdb3 100644 --- a/programs/compressed-token/anchor/src/lib.rs +++ b/programs/compressed-token/anchor/src/lib.rs @@ -580,6 +580,8 @@ pub enum ErrorCode { AuthorityTypeNotSupported, // 6165 (SPL Token code 15) #[msg("Mint decimals mismatch between the client and mint")] MintDecimalsMismatch, // 6166 (SPL Token code 18) + #[msg("Failed to calculate rent exemption for CMint")] + CMintRentExemptionFailed, // 6167 } /// Anchor error code offset - error codes start at 6000 diff --git a/programs/compressed-token/program/docs/ctoken/MINT_TO.md b/programs/compressed-token/program/docs/ctoken/MINT_TO.md index e7767a26d3..20f7e23363 100644 --- a/programs/compressed-token/program/docs/ctoken/MINT_TO.md +++ b/programs/compressed-token/program/docs/ctoken/MINT_TO.md @@ -13,7 +13,7 @@ Account layouts: - `CompressionInfo` extension defined in: program-libs/compressible/src/compression_info.rs **Instruction data:** -Path: programs/compressed-token/program/src/ctoken/mint_to.rs (lines 10-47) +Path: programs/compressed-token/program/src/ctoken/mint_to.rs (see `process_ctoken_mint_to` function) Byte layout: - Bytes 0-7: `amount` (u64, little-endian) - Number of tokens to mint diff --git a/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs index 9831c3bc3c..412defe297 100644 --- a/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs +++ b/programs/compressed-token/program/src/compressed_token/mint_action/mint_output.rs @@ -140,8 +140,8 @@ fn serialize_decompressed_mint( .ok_or(ErrorCode::CMintNotFound)?; let num_bytes = cmint_account.data_len() as u64; let current_lamports = cmint_account.lamports(); - let rent_exemption = get_rent_exemption_lamports(num_bytes) - .map_err(|_| ErrorCode::CMintTopUpCalculationFailed)?; + let rent_exemption = + get_rent_exemption_lamports(num_bytes).map_err(|_| ErrorCode::CMintRentExemptionFailed)?; // Skip top-up calculation if decompress mint action is present // (rent was just paid during account creation). diff --git a/programs/compressed-token/program/src/ctoken/create.rs b/programs/compressed-token/program/src/ctoken/create.rs index 6bd6a3d473..32ff326756 100644 --- a/programs/compressed-token/program/src/ctoken/create.rs +++ b/programs/compressed-token/program/src/ctoken/create.rs @@ -90,8 +90,9 @@ pub fn process_create_token_account( false, )?) } else { - // Non-compressible account: token_account must already exist and be owned by our program - // This is SPL-compatible initialize_account3 behavior + // Non-compressible account: token_account must already exist and be owned by CToken program. + // Unlike SPL initialize_account3 (which expects System-owned), this expects a pre-existing + // CToken-owned account. Ownership is implicitly validated when writing to the account. None }; diff --git a/programs/compressed-token/program/src/lib.rs b/programs/compressed-token/program/src/lib.rs index 232e0b9a6c..a04e6d92dd 100644 --- a/programs/compressed-token/program/src/lib.rs +++ b/programs/compressed-token/program/src/lib.rs @@ -25,7 +25,9 @@ use ctoken::{ }; use crate::{ - compressed_token::mint_action::processor::process_mint_action, + compressed_token::{ + mint_action::processor::process_mint_action, transfer2::processor::process_transfer2, + }, convert_account_infos::convert_account_infos, }; @@ -126,8 +128,6 @@ impl From for InstructionType { #[cfg(not(feature = "cpi"))] use pinocchio::program_entrypoint; -use crate::compressed_token::transfer2::processor::process_transfer2; - #[cfg(not(feature = "cpi"))] program_entrypoint!(process_instruction); diff --git a/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs b/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs index 790d4996b4..c1b2fd01de 100644 --- a/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs +++ b/programs/compressed-token/program/src/shared/initialize_ctoken_account.rs @@ -301,7 +301,6 @@ fn configure_compression_info( // Only try to read decimals if mint has data (is initialized) if !mint_data.is_empty() { let owner = mint_account.owner(); - let decimals = mint_data.get(44); if !is_valid_mint(owner, &mint_data)? { msg!("Invalid mint account: not a valid mint"); @@ -310,7 +309,7 @@ fn configure_compression_info( // Mint layout: decimals at byte 44 for all token programs // (mint_authority option: 36, supply: 8) = 44 - compressible_ext.set_decimals(decimals.copied()); + compressible_ext.set_decimals(mint_data.get(44).copied()); } Ok(()) From 13d992b433709a669518bcda06ab81eed78a496b Mon Sep 17 00:00:00 2001 From: ananas Date: Wed, 7 Jan 2026 01:51:15 +0000 Subject: [PATCH 16/18] fix js --- js/compressed-token/src/v3/layout/layout-mint-action.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/js/compressed-token/src/v3/layout/layout-mint-action.ts b/js/compressed-token/src/v3/layout/layout-mint-action.ts index ed75d7ea6c..5c169ce306 100644 --- a/js/compressed-token/src/v3/layout/layout-mint-action.ts +++ b/js/compressed-token/src/v3/layout/layout-mint-action.ts @@ -173,7 +173,6 @@ const ActionLayoutV1 = rustEnum([ MintToCompressedActionLayout.replicate('mintToCompressed'), UpdateAuthorityLayout.replicate('updateMintAuthority'), UpdateAuthorityLayout.replicate('updateFreezeAuthority'), - CreateSplMintActionLayout.replicate('createSplMint'), MintToCTokenActionLayout.replicate('mintToCToken'), UpdateMetadataFieldActionLayout.replicate('updateMetadataField'), UpdateMetadataAuthorityActionLayout.replicate('updateMetadataAuthority'), From 8c2e9f16ef8fa7ea4cf0efdd86a47b7f2d2057f1 Mon Sep 17 00:00:00 2001 From: ananas Date: Wed, 7 Jan 2026 15:30:38 +0000 Subject: [PATCH 17/18] fix js tests --- js/compressed-token/tests/e2e/unwrap.test.ts | 2 + .../tests/unit/layout-mint-action.test.ts | 56 ------------------- 2 files changed, 2 insertions(+), 56 deletions(-) diff --git a/js/compressed-token/tests/e2e/unwrap.test.ts b/js/compressed-token/tests/e2e/unwrap.test.ts index 7819948e5f..97ad4d811d 100644 --- a/js/compressed-token/tests/e2e/unwrap.test.ts +++ b/js/compressed-token/tests/e2e/unwrap.test.ts @@ -94,6 +94,7 @@ describe('createUnwrapInstruction', () => { mint, BigInt(1000), tokenPoolInfo!, + TEST_TOKEN_DECIMALS, ); expect(ix).toBeDefined(); @@ -125,6 +126,7 @@ describe('createUnwrapInstruction', () => { mint, BigInt(500), tokenPoolInfo!, + TEST_TOKEN_DECIMALS, feePayer.publicKey, ); diff --git a/js/compressed-token/tests/unit/layout-mint-action.test.ts b/js/compressed-token/tests/unit/layout-mint-action.test.ts index ff323f41da..e01c180e1e 100644 --- a/js/compressed-token/tests/unit/layout-mint-action.test.ts +++ b/js/compressed-token/tests/unit/layout-mint-action.test.ts @@ -203,62 +203,6 @@ describe('layout-mint-action', () => { expect('updateMintAuthority' in decoded.actions[0]).toBe(true); }); - it('should encode and decode with createSplMint action', () => { - const mint = Keypair.generate().publicKey; - - const createSplMintAction: Action = { - createSplMint: { - mintBump: 254, - }, - }; - - const data: MintActionCompressedInstructionData = { - leafIndex: 0, - proveByIndex: false, - rootIndex: 0, - compressedAddress: Array(32).fill(0), - tokenPoolBump: 255, - tokenPoolIndex: 0, - maxTopUp: 0, - createMint: { - readOnlyAddressTrees: [1, 2, 3, 4], - readOnlyAddressTreeRootIndices: [10, 20, 30, 40], - }, - actions: [createSplMintAction], - proof: { - a: Array(32).fill(1), - b: Array(64).fill(2), - c: Array(32).fill(3), - }, - cpiContext: null, - mint: { - supply: 0n, - decimals: 9, - metadata: { - version: 1, - cmintDecompressed: false, - mint, - }, - mintAuthority: mint, - freezeAuthority: null, - extensions: null, - }, - }; - - const encoded = encodeMintActionInstructionData(data); - const decoded = decodeMintActionInstructionData(encoded); - - expect(decoded.actions.length).toBe(1); - expect('createSplMint' in decoded.actions[0]).toBe(true); - - const action = decoded.actions[0] as { - createSplMint: { mintBump: number }; - }; - expect(action.createSplMint.mintBump).toBe(254); - expect(decoded.createMint).not.toBe(null); - expect(decoded.proof).not.toBe(null); - }); - it('should encode and decode with multiple actions', () => { const mint = Keypair.generate().publicKey; const recipient = Keypair.generate().publicKey; From 4f2c674ff258fcaf8973f09ddc4ede5a1f7d04f2 Mon Sep 17 00:00:00 2001 From: ananas Date: Wed, 7 Jan 2026 16:19:49 +0000 Subject: [PATCH 18/18] fix: allow exactly MAX_COMPRESSIONS items in transfer2 Change validation from >= to > to fix off-by-one error that prevented using exactly MAX_COMPRESSIONS compression operations. --- .../program/src/compressed_token/transfer2/compression/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs index cb4458e47f..c80a39df17 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/mod.rs @@ -50,7 +50,7 @@ pub fn process_token_compression<'a>( compression_to_input: &[Option; MAX_COMPRESSIONS], ) -> Result<(), ProgramError> { if let Some(compressions) = inputs.compressions.as_ref() { - if compressions.len() >= MAX_COMPRESSIONS { + if compressions.len() > MAX_COMPRESSIONS { msg!( "Too many compressions: {} provided, maximum {} allowed", compressions.len(),