From 7964b73357998a6628b8956f81a6fe083b88e605 Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 13 Nov 2025 01:37:59 +0000 Subject: [PATCH 1/5] fix: top up or rent transfer mint to ctoken --- .../src/mint_action/actions/mint_to_ctoken.rs | 12 +-- .../mint_action/actions/process_actions.rs | 77 ++++++++++++++++--- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs b/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs index 7f2efbbfaa..b28931bb93 100644 --- a/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs +++ b/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs @@ -21,7 +21,7 @@ pub fn process_mint_to_ctoken_action( validated_accounts: &MintActionAccounts, packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, mint: Pubkey, -) -> Result<(), ProgramError> { +) -> Result, ProgramError> { check_authority( compressed_mint.base.mint_authority, validated_accounts.authority.key(), @@ -54,10 +54,12 @@ 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(()) + + // Capture top-up lamport amount if compressible extension present + let transfer_amount = compress_or_decompress_ctokens(inputs)?; + + // Return account index and amount if there's a transfer needed + Ok(transfer_amount.map(|amount| (action.recipient.account_index, amount))) } #[profile] diff --git a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs b/programs/compressed-token/program/src/mint_action/actions/process_actions.rs index 6211a23821..5196de1a9b 100644 --- a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs +++ b/programs/compressed-token/program/src/mint_action/actions/process_actions.rs @@ -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::{ @@ -9,16 +10,23 @@ 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}, }, }; @@ -35,6 +43,9 @@ pub fn process_actions<'a>( packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, compressed_mint: &mut CompressedMint, ) -> Result<(), ProgramError> { + // Array to accumulate transfer amounts by account index (max 40 packed accounts) + let mut transfer_map = [0u64; 40]; + // Start metadata authority with same value as mint authority for action in parsed_instruction_data.actions.iter() { match action { @@ -80,13 +91,27 @@ 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 = 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((account_index, amount)) = transfer { + if account_index as usize > 40 { + msg!( + "Too many compression transfers: {}, max 40 allowed", + account_index + ); + 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( @@ -112,5 +137,37 @@ pub fn process_actions<'a>( } } + // Build transfers array from deduplicated map + let transfers: ArrayVec = 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::, ProgramError>>()?; + + // Execute transfers if any exist + if !transfers.is_empty() { + let fee_payer = validated_accounts + .executing + .as_ref() + .and_then(|exec| Some(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(()) } From db27984915c99a460c63927fe1747721f2d8604b Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 13 Nov 2025 15:55:33 +0000 Subject: [PATCH 2/5] refactor: combine MintToCTokenAction and DecompressedRecipient --- .../src/instructions/mint_action/mint_to_ctoken.rs | 10 ++-------- .../src/mint_action/actions/mint_to_ctoken.rs | 6 +++--- .../src/mint_action/actions/process_actions.rs | 2 +- .../compressed-token/program/tests/mint_action.rs | 12 +++++------- .../src/instructions/mint_action/instruction.rs | 10 +++------- 5 files changed, 14 insertions(+), 26 deletions(-) diff --git a/program-libs/ctoken-types/src/instructions/mint_action/mint_to_ctoken.rs b/program-libs/ctoken-types/src/instructions/mint_action/mint_to_ctoken.rs index 710a3afe70..72e0dfca4a 100644 --- a/program-libs/ctoken-types/src/instructions/mint_action/mint_to_ctoken.rs +++ b/program-libs/ctoken-types/src/instructions/mint_action/mint_to_ctoken.rs @@ -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, -} diff --git a/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs b/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs index b28931bb93..7a131b0f58 100644 --- a/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs +++ b/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs @@ -28,7 +28,7 @@ pub fn process_mint_to_ctoken_action( "mint authority", )?; - let amount = u64::from(action.recipient.amount); + let amount = u64::from(action.amount); compressed_mint.base.supply = compressed_mint .base .supply @@ -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 @@ -59,7 +59,7 @@ pub fn process_mint_to_ctoken_action( let transfer_amount = compress_or_decompress_ctokens(inputs)?; // Return account index and amount if there's a transfer needed - Ok(transfer_amount.map(|amount| (action.recipient.account_index, amount))) + Ok(transfer_amount.map(|amount| (action.account_index, amount))) } #[profile] diff --git a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs b/programs/compressed-token/program/src/mint_action/actions/process_actions.rs index 5196de1a9b..39197057e4 100644 --- a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs +++ b/programs/compressed-token/program/src/mint_action/actions/process_actions.rs @@ -161,7 +161,7 @@ pub fn process_actions<'a>( let fee_payer = validated_accounts .executing .as_ref() - .and_then(|exec| Some(exec.system.fee_payer)) + .map(|exec| exec.system.fee_payer) .ok_or_else(|| { msg!("Fee payer required for compressible token account top-ups"); ProgramError::NotEnoughAccountKeys diff --git a/programs/compressed-token/program/tests/mint_action.rs b/programs/compressed-token/program/tests/mint_action.rs index df95b2e3f8..cecff31b48 100644 --- a/programs/compressed-token/program/tests/mint_action.rs +++ b/programs/compressed-token/program/tests/mint_action.rs @@ -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, @@ -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), } } diff --git a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs index e27aedfebf..525e440cc0 100644 --- a/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs +++ b/sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs @@ -338,9 +338,7 @@ 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); @@ -348,10 +346,8 @@ pub fn create_mint_action_cpi( decompressed_account_index += 1; program_actions.push(Action::MintToCToken(MintToCTokenAction { - recipient: DecompressedRecipient { - account_index: current_index, - amount, - }, + account_index: current_index, + amount, })); } MintActionType::UpdateMetadataField { From b59aa4032a0da5d6af6a682d6a8d9f1a27e1f544 Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 13 Nov 2025 17:22:03 +0000 Subject: [PATCH 3/5] test: assert mint to ctoken top up --- .../tests/mint/edge_cases.rs | 28 ++++-- program-tests/utils/src/assert_mint_action.rs | 90 ++++++++++++++++--- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/program-tests/compressed-token-test/tests/mint/edge_cases.rs b/program-tests/compressed-token-test/tests/mint/edge_cases.rs index ebec14349a..0ab7e7ffc5 100644 --- a/program-tests/compressed-token-test/tests/mint/edge_cases.rs +++ b/program-tests/compressed-token-test/tests/mint/edge_cases.rs @@ -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, @@ -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(); diff --git a/program-tests/utils/src/assert_mint_action.rs b/program-tests/utils/src/assert_mint_action.rs index 1dc07af81d..a19dda082a 100644 --- a/program-tests/utils/src/assert_mint_action.rs +++ b/program-tests/utils/src/assert_mint_action.rs @@ -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 /// @@ -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 + ); + } } } From e4b0e061438d435a8a2e385db66b179b850bf2f8 Mon Sep 17 00:00:00 2001 From: ananas-block <58553958+ananas-block@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:02:36 +0000 Subject: [PATCH 4/5] Update programs/compressed-token/program/src/mint_action/actions/process_actions.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Swen Schäferjohann --- .../program/src/mint_action/actions/process_actions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs b/programs/compressed-token/program/src/mint_action/actions/process_actions.rs index 39197057e4..04c948b0ea 100644 --- a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs +++ b/programs/compressed-token/program/src/mint_action/actions/process_actions.rs @@ -101,7 +101,7 @@ pub fn process_actions<'a>( // Accumulate transfer amount if present (deduplication happens here) if let Some((account_index, amount)) = transfer { - if account_index as usize > 40 { + if account_index as usize >= 40 { msg!( "Too many compression transfers: {}, max 40 allowed", account_index From 3b8e76a575914b2c10e0d973720bbee494b667f1 Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 13 Nov 2025 19:11:40 +0000 Subject: [PATCH 5/5] cleanup --- .../src/mint_action/actions/mint_to_ctoken.rs | 8 ++----- .../mint_action/actions/process_actions.rs | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs b/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs index 7a131b0f58..375d05150a 100644 --- a/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs +++ b/programs/compressed-token/program/src/mint_action/actions/mint_to_ctoken.rs @@ -21,7 +21,7 @@ pub fn process_mint_to_ctoken_action( validated_accounts: &MintActionAccounts, packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, mint: Pubkey, -) -> Result, ProgramError> { +) -> Result, ProgramError> { check_authority( compressed_mint.base.mint_authority, validated_accounts.authority.key(), @@ -55,11 +55,7 @@ pub fn process_mint_to_ctoken_action( packed_accounts, ); - // Capture top-up lamport amount if compressible extension present - let transfer_amount = compress_or_decompress_ctokens(inputs)?; - - // Return account index and amount if there's a transfer needed - Ok(transfer_amount.map(|amount| (action.account_index, amount))) + compress_or_decompress_ctokens(inputs) } #[profile] diff --git a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs b/programs/compressed-token/program/src/mint_action/actions/process_actions.rs index 04c948b0ea..3eb6bd7012 100644 --- a/programs/compressed-token/program/src/mint_action/actions/process_actions.rs +++ b/programs/compressed-token/program/src/mint_action/actions/process_actions.rs @@ -30,6 +30,9 @@ use crate::{ }, }; +/// 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>( @@ -43,8 +46,8 @@ pub fn process_actions<'a>( packed_accounts: &ProgramPackedAccounts<'_, AccountInfo>, compressed_mint: &mut CompressedMint, ) -> Result<(), ProgramError> { - // Array to accumulate transfer amounts by account index (max 40 packed accounts) - let mut transfer_map = [0u64; 40]; + // 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() { @@ -91,7 +94,7 @@ pub fn process_actions<'a>( // compressed_mint.metadata.spl_mint_initialized = true; } ZAction::MintToCToken(mint_to_ctoken_action) => { - let transfer = process_mint_to_ctoken_action( + let transfer_amount = process_mint_to_ctoken_action( mint_to_ctoken_action, compressed_mint, validated_accounts, @@ -100,11 +103,13 @@ pub fn process_actions<'a>( )?; // Accumulate transfer amount if present (deduplication happens here) - if let Some((account_index, amount)) = transfer { - if account_index as usize >= 40 { + 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 40 allowed", - account_index + "Too many compression transfers: {}, max {} allowed", + account_index, + MAX_PACKED_ACCOUNTS ); return Err(ErrorCode::TooManyCompressionTransfers.into()); } @@ -138,7 +143,7 @@ pub fn process_actions<'a>( } // Build transfers array from deduplicated map - let transfers: ArrayVec = transfer_map + let transfers: ArrayVec = transfer_map .iter() .enumerate() .filter_map(|(index, &amount)| { @@ -154,7 +159,7 @@ pub fn process_actions<'a>( amount, }) }) - .collect::, ProgramError>>()?; + .collect::, ProgramError>>()?; // Execute transfers if any exist if !transfers.is_empty() {