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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

160 changes: 160 additions & 0 deletions program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,163 @@ async fn test_spl_to_ctoken_transfer() {

println!("Successfully completed round-trip transfer: SPL -> CToken -> SPL");
}

#[tokio::test]
async fn test_ctoken_to_spl_with_compress_and_close() {
use light_compressed_token_sdk::{
instructions::create_ctoken_to_spl_transfer_and_close_instruction,
token_pool::find_token_pool_pda_with_index,
};

let mut rpc = LightProgramTest::new(ProgramTestConfig::new(true, None))
.await
.unwrap();
let payer = rpc.get_payer().insecure_clone();
let sender = Keypair::new();
airdrop_lamports(&mut rpc, &sender.pubkey(), 1_000_000_000)
.await
.unwrap();
let mint = create_mint_helper(&mut rpc, &payer).await;
let amount = 10000u64;
let transfer_amount = 5000u64;

// Create SPL token account and mint tokens
let spl_token_account_keypair = Keypair::new();
create_token_2022_account(&mut rpc, &mint, &spl_token_account_keypair, &sender, false)
.await
.unwrap();
mint_spl_tokens(
&mut rpc,
&mint,
&spl_token_account_keypair.pubkey(),
&payer.pubkey(),
&payer,
amount,
false,
)
.await
.unwrap();

// Create recipient for compressed tokens
let recipient = Keypair::new();
airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000)
.await
.unwrap();

// Create compressed token ATA for recipient
let instruction = light_compressed_token_sdk::instructions::create_associated_token_account(
payer.pubkey(),
recipient.pubkey(),
mint,
)
.map_err(|e| RpcError::AssertRpcError(format!("Failed to create ATA instruction: {}", e)))
.unwrap();
rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer])
.await
.unwrap();
let associated_token_account = derive_ctoken_ata(&recipient.pubkey(), &mint).0;

// Transfer SPL to CToken
transfer2::spl_to_ctoken_transfer(
&mut rpc,
spl_token_account_keypair.pubkey(),
associated_token_account,
transfer_amount,
&sender,
&payer,
)
.await
.unwrap();

// Verify compressed token balance after initial transfer
{
let ctoken_account_data = rpc
.get_account(associated_token_account)
.await
.unwrap()
.unwrap();
let ctoken_account =
spl_pod::bytemuck::pod_from_bytes::<PodAccount>(&ctoken_account_data.data[..165])
.map_err(|e| {
RpcError::AssertRpcError(format!("Failed to parse CToken account: {}", e))
})
.unwrap();
assert_eq!(
u64::from(ctoken_account.amount),
transfer_amount,
"Recipient should have {} compressed tokens",
transfer_amount
);
}

// Now transfer back using CompressAndClose instead of regular transfer
println!("Testing reverse transfer with CompressAndClose: ctoken to SPL");

// Get token pool PDA
let (token_pool_pda, token_pool_pda_bump) = find_token_pool_pda_with_index(&mint, 0);

// Create instruction using compress_and_close variant
// Note: Using spl_token::ID because create_mint_helper creates Token (not Token-2022) mints
let transfer_ix = create_ctoken_to_spl_transfer_and_close_instruction(
associated_token_account,
spl_token_account_keypair.pubkey(),
transfer_amount,
recipient.pubkey(),
mint,
payer.pubkey(),
token_pool_pda,
token_pool_pda_bump,
anchor_spl::token::ID,
)
.unwrap();

// Execute transaction
rpc.create_and_send_transaction(&[transfer_ix], &payer.pubkey(), &[&payer, &recipient])
.await
.unwrap();

// Verify final balances
{
// Verify SPL token balance is restored
let spl_account_data = rpc
.get_account(spl_token_account_keypair.pubkey())
.await
.unwrap()
.unwrap();
let spl_account = spl_pod::bytemuck::pod_from_bytes::<PodAccount>(&spl_account_data.data)
.map_err(|e| {
RpcError::AssertRpcError(format!("Failed to parse SPL token account: {}", e))
})
.unwrap();
let restored_spl_balance: u64 = spl_account.amount.into();
assert_eq!(
restored_spl_balance, amount,
"SPL token balance should be restored to original amount"
);
}

{
// Verify CToken account is CLOSED (not just balance = 0)
let ctoken_account_result = rpc.get_account(associated_token_account).await.unwrap();
match ctoken_account_result {
None => {
println!("✓ CToken account successfully closed (account does not exist)");
}
Some(account_data) => {
assert_eq!(
account_data.data.len(),
0,
"CToken account data should be empty after CompressAndClose"
);
assert_eq!(
account_data.lamports, 0,
"CToken account lamports should be 0 after CompressAndClose"
);
println!("✓ CToken account successfully closed (zeroed out)");
}
}
}

println!("✓ Successfully completed CToken -> SPL transfer with CompressAndClose");
println!(" This validates owner can use CompressAndClose without explicit compressed_token_account validation");
}
5 changes: 2 additions & 3 deletions program-tests/registry-test/tests/compressible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use light_test_utils::{
airdrop_lamports, assert_claim::assert_claim, spl::create_mint_helper, Rpc, RpcError,
};
use light_token_client::actions::{
create_compressible_token_account, CreateCompressibleTokenAccountInputs,
create_compressible_token_account, transfer_ctoken, CreateCompressibleTokenAccountInputs,
};
use solana_sdk::{
instruction::Instruction,
Expand Down Expand Up @@ -1140,7 +1140,6 @@ async fn assert_not_compressible<R: Rpc>(
#[tokio::test]
async fn test_compressible_account_infinite_funding() -> Result<(), RpcError> {
use light_test_utils::assert_ctoken_transfer::assert_ctoken_transfer;
use light_token_client::actions::ctoken_transfer;

let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2(false, None))
.await
Expand Down Expand Up @@ -1281,7 +1280,7 @@ async fn test_compressible_account_infinite_funding() -> Result<(), RpcError> {
};

// Transfer all tokens from source to dest
ctoken_transfer(
transfer_ctoken(
&mut rpc,
source,
dest,
Expand Down
4 changes: 4 additions & 0 deletions programs/compressed-token/anchor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,10 @@ pub enum ErrorCode {
MintActionInvalidCpiContextAddressTreePubkey,
#[msg("CompressAndClose: Cannot use the same compressed output account for multiple closures")]
CompressAndCloseDuplicateOutput,
#[msg(
"CompressAndClose by compression authority requires compressed token account in outputs"
)]
CompressAndCloseOutputMissing,
}

impl From<ErrorCode> for ProgramError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ pub fn process_compress_and_close(

if compression_authority_is_signer {
// Compress the complete balance to this compressed token account.
let compressed_account = close_inputs
.compressed_token_account
.ok_or(ErrorCode::CompressAndCloseOutputMissing)?;
validate_compressed_token_account(
packed_accounts,
amount,
close_inputs.compressed_token_account,
compressed_account,
ctoken,
compress_to_pubkey,
token_account_info.key(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
use light_account_checks::packed_accounts::ProgramPackedAccounts;
use light_ctoken_types::{
instructions::transfer2::{
ZCompressedTokenInstructionDataTransfer2, ZCompression, ZCompressionMode,
ZMultiTokenTransferOutputData,
},
CTokenError,
use light_ctoken_types::instructions::transfer2::{
ZCompressedTokenInstructionDataTransfer2, ZCompression, ZCompressionMode,
ZMultiTokenTransferOutputData,
};
use pinocchio::{account_info::AccountInfo, pubkey::Pubkey};

/// Compress and close specific inputs
pub struct CompressAndCloseInputs<'a> {
pub destination: &'a AccountInfo,
pub rent_sponsor: &'a AccountInfo,
pub compressed_token_account: &'a ZMultiTokenTransferOutputData<'a>,
pub compressed_token_account: Option<&'a ZMultiTokenTransferOutputData<'a>>,
}

/// Input struct for ctoken compression/decompression operations
Expand Down Expand Up @@ -60,8 +57,7 @@ impl<'a> CTokenCompressionInputs<'a> {
)?,
compressed_token_account: inputs
.out_token_data
.get(compression.get_compressed_token_account_index()? as usize)
.ok_or(CTokenError::AccountFrozen)?,
.get(compression.get_compressed_token_account_index()? as usize),
})
} else {
None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,6 @@ fn spl_token_transfer_invoke_cpi(
cpi_authority: &AccountInfo,
amount: u64,
) -> Result<(), ProgramError> {
msg!("spl_token_transfer_invoke_cpi");
msg!(
"from {:?}",
solana_pubkey::Pubkey::new_from_array(*from.key())
);
msg!("to {:?}", solana_pubkey::Pubkey::new_from_array(*to.key()));
msg!("amount {:?}", amount);
let bump_seed = [BUMP_CPI_AUTHORITY];
let seed_array = [
Seed::from(CPI_AUTHORITY_PDA_SEED),
Expand All @@ -110,13 +103,6 @@ fn spl_token_transfer_invoke(
authority: &AccountInfo,
amount: u64,
) -> Result<(), ProgramError> {
msg!("spl_token_transfer_invoke");
msg!(
"from {:?}",
solana_pubkey::Pubkey::new_from_array(*from.key())
);
msg!("to {:?}", solana_pubkey::Pubkey::new_from_array(*to.key()));
msg!("amount {:?}", amount);
spl_token_transfer_common(program_id, from, to, authority, amount, None)
}

Expand Down
4 changes: 2 additions & 2 deletions sdk-libs/compressed-token-sdk/src/instructions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ pub use mint_to_compressed::{
};
pub use transfer_ctoken::{transfer_ctoken, transfer_ctoken_signed};
pub use transfer_interface::{
create_transfer_ctoken_to_spl_instruction, create_transfer_spl_to_ctoken_instruction,
transfer_interface, transfer_interface_signed,
create_ctoken_to_spl_transfer_and_close_instruction, create_transfer_ctoken_to_spl_instruction,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Address the TODO in the implementation and add documentation.

Based on the relevant code snippet showing the implementation in transfer_interface.rs (lines 163-235), two improvements are needed:

  1. TODO for dynamic pool_index: Line 210 contains pool_index (TODO: make dynamic) currently hardcoded to 0. This may work for single-pool scenarios but could be a limitation for multi-pool support.

  2. Missing documentation: This new public API function has 9 parameters but no doc comments explaining:

    • The purpose and use case (compress-close-decompress flow)
    • What each parameter represents
    • When to use this vs create_transfer_ctoken_to_spl_instruction
    • Any constraints or assumptions (like the hardcoded pool_index)

Would you like me to generate documentation for this function, or should the pool_index TODO be tracked in a separate issue?

🤖 Prompt for AI Agents
In sdk-libs/compressed-token-sdk/src/instructions/mod.rs around line 57,
implement the TODO for the hardcoded pool_index and add doc comments for the
public API: change the implementation so pool_index is not fixed to 0 (either
accept pool_index as an additional parameter to the public function or compute
it dynamically from the provided account metas/merkle data and validate it),
update all call sites (transfer_interface.rs and related helpers) to pass or
derive the correct pool_index, and add a complete doc comment block for the
function that explains its purpose (compress-close-decompress flow), documents
all 9 parameters (types and role), states when to use this function vs
create_transfer_ctoken_to_spl_instruction, and lists constraints/assumptions
including how pool_index is determined and any multisig/multi-pool limitations.

create_transfer_spl_to_ctoken_instruction, transfer_interface, transfer_interface_signed,
};
pub use update_compressed_mint::{
update_compressed_mint, update_compressed_mint_cpi, UpdateCompressedMintInputs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,82 @@ pub fn create_transfer_ctoken_to_spl_instruction(
create_transfer2_instruction(inputs)
}

#[allow(clippy::too_many_arguments)]
#[profile]
pub fn create_ctoken_to_spl_transfer_and_close_instruction(
source_ctoken_account: Pubkey,
destination_spl_token_account: Pubkey,
amount: u64,
authority: Pubkey,
mint: Pubkey,
payer: Pubkey,
token_pool_pda: Pubkey,
token_pool_pda_bump: u8,
spl_token_program: Pubkey,
) -> Result<Instruction, TokenSdkError> {
let packed_accounts = vec![
// Mint (index 0)
AccountMeta::new_readonly(mint, false),
// Source ctoken account (index 1) - writable
AccountMeta::new(source_ctoken_account, false),
// Destination SPL token account (index 2) - writable
AccountMeta::new(destination_spl_token_account, false),
// Authority (index 3) - signer
AccountMeta::new(authority, true),
// Token pool PDA (index 4) - writable
AccountMeta::new(token_pool_pda, false),
// SPL Token program (index 5) - needed for CPI
AccountMeta::new_readonly(spl_token_program, false),
];

// First operation: compress from ctoken account to pool using compress_and_close
let compress_to_pool = CTokenAccount2 {
inputs: vec![],
output: MultiTokenTransferOutputData::default(),
compression: Some(Compression::compress_and_close_ctoken(
amount, 0, // mint index
1, // source ctoken account index
3, // authority index
0, // no rent sponsor
0, // no compressed account
3, // destination is authority
)),
delegate_is_set: false,
method_used: true,
};

// Second operation: decompress from pool to SPL token account using decompress_spl
let decompress_to_spl = CTokenAccount2 {
inputs: vec![],
output: MultiTokenTransferOutputData::default(),
compression: Some(Compression::decompress_spl(
amount,
0, // mint index
2, // destination SPL token account index
4, // pool_account_index
0, // pool_index (TODO: make dynamic)
token_pool_pda_bump,
)),
delegate_is_set: false,
method_used: true,
};

let inputs = Transfer2Inputs {
validity_proof: ValidityProof::new(None),
transfer_config: Transfer2Config::default().filter_zero_amount_outputs(),
meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only(
payer,
packed_accounts,
),
in_lamports: None,
out_lamports: None,
token_accounts: vec![compress_to_pool, decompress_to_spl],
output_queue: 0, // Decompressed accounts only, no output queue needed
};

create_transfer2_instruction(inputs)
}

/// Transfer SPL tokens to compressed tokens
#[allow(clippy::too_many_arguments)]
pub fn transfer_spl_to_ctoken<'info>(
Expand Down
Loading