Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,8 @@ use light_zero_copy::ZeroCopy;
use crate::{AnchorDeserialize, AnchorSerialize};

#[repr(C)]
#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)]
pub struct DecompressedRecipient {
#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, ZeroCopy)]
pub struct MintToCTokenAction {
pub account_index: u8, // Index into remaining accounts for the recipient token account
pub amount: u64,
}

#[repr(C)]
#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy)]
pub struct MintToCTokenAction {
pub recipient: DecompressedRecipient,
}
28 changes: 21 additions & 7 deletions program-tests/compressed-token-test/tests/mint/edge_cases.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use anchor_lang::prelude::borsh::BorshDeserialize;
use light_client::indexer::Indexer;
use light_compressed_token_sdk::instructions::{
create_associated_token_account::{
create_compressible_associated_token_account,
CreateCompressibleAssociatedTokenAccountInputs,
},
derive_compressed_mint_address, find_spl_mint_address,
};
use light_ctoken_types::state::{extensions::AdditionalMetadata, CompressedMint};
use light_ctoken_types::state::{extensions::AdditionalMetadata, CompressedMint, TokenDataVersion};
use light_program_test::{LightProgramTest, ProgramTestConfig};
use light_test_utils::{
assert_mint_action::assert_mint_action, mint_assert::assert_compressed_mint_account, Rpc,
Expand Down Expand Up @@ -139,16 +143,26 @@ async fn functional_all_in_one_instruction() {
let new_freeze_authority = Keypair::new();
let new_metadata_authority = Keypair::new();

// Create a ctoken account for MintToCToken
// Create a compressible ctoken account for MintToCToken
let recipient = Keypair::new();
let create_ata_ix = light_compressed_token_sdk::instructions::create_associated_token_account(
payer.pubkey(),
recipient.pubkey(),
spl_mint_pda,
let create_compressible_ata_ix = create_compressible_associated_token_account(
CreateCompressibleAssociatedTokenAccountInputs {
payer: payer.pubkey(),
owner: recipient.pubkey(),
mint: spl_mint_pda,
rent_sponsor: rpc.test_accounts.funding_pool_config.rent_sponsor_pda,
pre_pay_num_epochs: 0,
lamports_per_write: Some(1000),
compressible_config: rpc
.test_accounts
.funding_pool_config
.compressible_config_pda,
token_account_version: TokenDataVersion::ShaFlat,
},
)
.unwrap();

rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer])
rpc.create_and_send_transaction(&[create_compressible_ata_ix], &payer.pubkey(), &[&payer])
.await
.unwrap();

Expand Down
90 changes: 78 additions & 12 deletions program-tests/utils/src/assert_mint_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ use light_client::indexer::Indexer;
use light_compressed_token_sdk::instructions::mint_action::MintActionType;
use light_ctoken_types::state::{
extensions::{AdditionalMetadata, ExtensionStruct},
CompressedMint,
CToken, CompressedMint,
};
use light_program_test::LightProgramTest;
use solana_sdk::{program_pack::Pack, pubkey::Pubkey};
use light_program_test::{LightProgramTest, Rpc};
use solana_sdk::pubkey::Pubkey;

/// Assert that mint actions produce the expected state changes
///
Expand Down Expand Up @@ -141,22 +141,88 @@ pub async fn assert_mint_action(
let pre_account = rpc
.get_pre_transaction_account(&account_pubkey)
.expect("CToken account should exist before minting");
let mut expected_token_account =
spl_token::state::Account::unpack(&pre_account.data[..165]).unwrap();

// Parse pre-transaction CToken state
let mut pre_ctoken: CToken =
BorshDeserialize::deserialize(&mut &pre_account.data[..]).unwrap();

// Apply the total minted amount (handles multiple mints to same account)
expected_token_account.amount += total_minted_amount;
pre_ctoken.amount = pre_ctoken
.amount
.checked_add(total_minted_amount)
.expect("Token amount overflow");

// Get actual post-transaction account
let account_data = rpc.context.get_account(&account_pubkey).unwrap();
let actual_token_account =
spl_token::state::Account::unpack(&account_data.data[..165]).unwrap();
let post_ctoken: CToken =
BorshDeserialize::deserialize(&mut &account_data.data[..]).unwrap();

// Single assertion for complete account state
// Assert token amount matches expected
assert_eq!(
actual_token_account, expected_token_account,
"CToken account state at {} should match expected after minting {} tokens",
account_pubkey, total_minted_amount
post_ctoken.amount, pre_ctoken.amount,
"CToken account state at {} should have {} tokens after minting, got {}",
account_pubkey, pre_ctoken.amount, post_ctoken.amount
);

// Validate lamport balance changes for compressible accounts
let pre_lamports = pre_account.lamports;
let post_lamports = account_data.lamports;

// Check if account has compressible extension (reuse pre_ctoken parsed earlier)
if let Some(extensions) = pre_ctoken.extensions.as_ref() {
// Look for compressible extension
let compressible_ext = extensions.iter().find_map(|ext| {
if let ExtensionStruct::Compressible(comp) = ext {
Some(comp)
} else {
None
}
});

if let Some(compressible) = compressible_ext {
// Account has compressible extension - calculate expected top-up
let current_slot = rpc.get_slot().await.unwrap();
let account_size = pre_account.data.len() as u64;

let expected_top_up = compressible
.calculate_top_up_lamports(
account_size,
current_slot,
pre_lamports,
compressible.lamports_per_write,
light_ctoken_types::COMPRESSIBLE_TOKEN_RENT_EXEMPTION,
)
.unwrap();

let actual_lamport_change = post_lamports
.checked_sub(pre_lamports)
.expect("Post lamports should be >= pre lamports");

assert_eq!(
actual_lamport_change, expected_top_up,
"CToken account at {} should receive {} lamports top-up for compressible extension, got {}",
account_pubkey, expected_top_up, actual_lamport_change
);

println!(
"✓ Lamport top-up validated: {} lamports transferred to compressible ctoken account {}",
expected_top_up, account_pubkey
);
} else {
// Has extensions but no compressible extension - lamports should not change
assert_eq!(
pre_lamports, post_lamports,
"Non-compressible CToken account at {} should not receive lamport top-up",
account_pubkey
);
}
} else {
// No extensions - lamports should not change
assert_eq!(
pre_lamports, post_lamports,
"CToken account without extensions at {} should not receive lamport top-up",
account_pubkey
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ pub fn process_mint_to_ctoken_action(
validated_accounts: &MintActionAccounts,
packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>,
mint: Pubkey,
) -> Result<(), ProgramError> {
) -> Result<Option<u64>, ProgramError> {
check_authority(
compressed_mint.base.mint_authority,
validated_accounts.authority.key(),
"mint authority",
)?;

let amount = u64::from(action.recipient.amount);
let amount = u64::from(action.amount);
compressed_mint.base.supply = compressed_mint
.base
.supply
Expand All @@ -44,7 +44,7 @@ pub fn process_mint_to_ctoken_action(

// Get the recipient token account from packed accounts using the index
let token_account_info =
packed_accounts.get_u8(action.recipient.account_index, "ctoken mint to recipient")?;
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
Expand All @@ -54,10 +54,8 @@ pub fn process_mint_to_ctoken_action(
token_account_info,
packed_accounts,
);
// For mint_to_ctoken, we don't need to handle lamport transfers
// as there's no compressible extension on the target account
compress_or_decompress_ctokens(inputs)?;
Ok(())

compress_or_decompress_ctokens(inputs)
}

#[profile]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anchor_compressed_token::ErrorCode;
use anchor_lang::prelude::ProgramError;
use arrayvec::ArrayVec;
use light_account_checks::packed_accounts::ProgramPackedAccounts;
use light_compressed_account::instruction_data::data::ZOutputCompressedAccountWithPackedContextMut;
use light_ctoken_types::{
Expand All @@ -9,19 +10,29 @@ use light_ctoken_types::{
};
use light_program_profiler::profile;
use pinocchio::account_info::AccountInfo;
use spl_pod::solana_msg::msg;

use crate::mint_action::{
accounts::MintActionAccounts,
check_authority,
mint_to::process_mint_to_compressed_action,
mint_to_ctoken::process_mint_to_ctoken_action,
queue_indices::QueueIndices,
update_metadata::{
process_remove_metadata_key_action, process_update_metadata_authority_action,
process_update_metadata_field_action,
use crate::{
mint_action::{
accounts::MintActionAccounts,
check_authority,
mint_to::process_mint_to_compressed_action,
mint_to_ctoken::process_mint_to_ctoken_action,
queue_indices::QueueIndices,
update_metadata::{
process_remove_metadata_key_action, process_update_metadata_authority_action,
process_update_metadata_field_action,
},
},
shared::{
convert_program_error,
transfer_lamports::{multi_transfer_lamports, Transfer},
},
};

/// Maximum number of packed accounts allowed in a single instruction
const MAX_PACKED_ACCOUNTS: usize = 40;

#[allow(clippy::too_many_arguments)]
#[profile]
pub fn process_actions<'a>(
Expand All @@ -35,6 +46,9 @@ pub fn process_actions<'a>(
packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>,
compressed_mint: &mut CompressedMint,
) -> Result<(), ProgramError> {
// Array to accumulate transfer amounts by account index
let mut transfer_map = [0u64; MAX_PACKED_ACCOUNTS];

// Start metadata authority with same value as mint authority
for action in parsed_instruction_data.actions.iter() {
match action {
Expand Down Expand Up @@ -80,13 +94,29 @@ pub fn process_actions<'a>(
// compressed_mint.metadata.spl_mint_initialized = true;
}
ZAction::MintToCToken(mint_to_ctoken_action) => {
process_mint_to_ctoken_action(
let transfer_amount = process_mint_to_ctoken_action(
mint_to_ctoken_action,
compressed_mint,
validated_accounts,
packed_accounts,
parsed_instruction_data.mint.metadata.mint,
)?;

// Accumulate transfer amount if present (deduplication happens here)
if let Some(amount) = transfer_amount {
let account_index = mint_to_ctoken_action.account_index;
if account_index as usize >= MAX_PACKED_ACCOUNTS {
msg!(
"Too many compression transfers: {}, max {} allowed",
account_index,
MAX_PACKED_ACCOUNTS
);
return Err(ErrorCode::TooManyCompressionTransfers.into());
}
transfer_map[account_index as usize] = transfer_map[account_index as usize]
.checked_add(amount)
.ok_or(ProgramError::ArithmeticOverflow)?;
}
}
ZAction::UpdateMetadataField(update_metadata_action) => {
process_update_metadata_field_action(
Expand All @@ -112,5 +142,37 @@ pub fn process_actions<'a>(
}
}

// Build transfers array from deduplicated map
let transfers: ArrayVec<Transfer, MAX_PACKED_ACCOUNTS> = transfer_map
.iter()
.enumerate()
.filter_map(|(index, &amount)| {
if amount != 0 {
Some((index as u8, amount))
} else {
None
}
})
.map(|(index, amount)| {
Ok(Transfer {
account: packed_accounts.get_u8(index, "transfer account")?,
amount,
})
})
.collect::<Result<ArrayVec<Transfer, MAX_PACKED_ACCOUNTS>, ProgramError>>()?;

// Execute transfers if any exist
if !transfers.is_empty() {
let fee_payer = validated_accounts
.executing
.as_ref()
.map(|exec| exec.system.fee_payer)
.ok_or_else(|| {
msg!("Fee payer required for compressible token account top-ups");
ProgramError::NotEnoughAccountKeys
})?;
multi_transfer_lamports(fee_payer, &transfers).map_err(convert_program_error)?;
}

Ok(())
}
12 changes: 5 additions & 7 deletions programs/compressed-token/program/tests/mint_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use light_ctoken_types::{
extensions::{token_metadata::TokenMetadataInstructionData, ExtensionInstructionData},
mint_action::{
Action, CompressedMintInstructionData, CpiContext, CreateMint, CreateSplMintAction,
DecompressedRecipient, MintActionCompressedInstructionData, MintToCTokenAction,
MintToCompressedAction, Recipient, RemoveMetadataKeyAction, UpdateAuthority,
UpdateMetadataAuthorityAction, UpdateMetadataFieldAction,
MintActionCompressedInstructionData, MintToCTokenAction, MintToCompressedAction,
Recipient, RemoveMetadataKeyAction, UpdateAuthority, UpdateMetadataAuthorityAction,
UpdateMetadataFieldAction,
},
},
state::CompressedMintMetadata,
Expand Down Expand Up @@ -72,10 +72,8 @@ fn random_mint_to_action(rng: &mut StdRng) -> MintToCompressedAction {

fn random_mint_to_decompressed_action(rng: &mut StdRng) -> MintToCTokenAction {
MintToCTokenAction {
recipient: DecompressedRecipient {
amount: rng.gen_range(1..=1_000_000),
account_index: rng.gen_range(1..=255),
},
amount: rng.gen_range(1..=1_000_000),
account_index: rng.gen_range(1..=255),
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,20 +338,16 @@ pub fn create_mint_action_cpi(
}));
}
MintActionType::MintToCToken { account, amount } => {
use light_ctoken_types::instructions::mint_action::{
DecompressedRecipient, MintToCTokenAction,
};
use light_ctoken_types::instructions::mint_action::MintToCTokenAction;

// Add account to decompressed accounts list and get its index
decompressed_accounts.push(account);
let current_index = decompressed_account_index;
decompressed_account_index += 1;

program_actions.push(Action::MintToCToken(MintToCTokenAction {
recipient: DecompressedRecipient {
account_index: current_index,
amount,
},
account_index: current_index,
amount,
}));
}
MintActionType::UpdateMetadataField {
Expand Down
Loading