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
158 changes: 117 additions & 41 deletions program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,60 @@ use super::shared::*;
#[tokio::test]
#[serial]
async fn test_compress_and_close_owner_scenarios() {
// Test 1: Owner closes account with token balance
// Test 1: Owner cannot close compressible account with token balance
// Only compression_authority can compress and close compressible accounts
{
let mut context = setup_compress_and_close_test(
2, // 2 prepaid epochs
1000, // 1000 token balance
None, // No time warp needed for owner
None, // No time warp needed
false, // Use default rent sponsor
)
.await
.unwrap();

compress_and_close_owner_and_assert(
// Clone owner keypair before mutable borrow
let owner_keypair = context.owner_keypair.insecure_clone();

// Owner trying to compress and close should fail with InvalidAccountData
compress_and_close_and_assert_fails(
&mut context,
&owner_keypair,
None, // Default destination (owner)
"owner_with_balance",
"owner_with_balance_should_fail",
3, // ProgramError::InvalidAccountData
)
.await;
}

// Test 2: Owner closes account with zero balance
// Test 2: Owner cannot close compressible account with zero balance
// Only compression_authority can compress and close compressible accounts
{
let mut context = setup_compress_and_close_test(
2, // 2 prepaid epochs
0, // Zero token balance
None, // No time warp needed for owner
None, // No time warp needed
false, // Use default rent sponsor
)
.await
.unwrap();

compress_and_close_owner_and_assert(
// Clone owner keypair before mutable borrow
let owner_keypair = context.owner_keypair.insecure_clone();

// Owner trying to compress and close should fail with InvalidAccountData
compress_and_close_and_assert_fails(
&mut context,
&owner_keypair,
None, // Default destination (owner)
"owner_zero_balance",
"owner_zero_balance_should_fail",
3, // ProgramError::InvalidAccountData
)
.await;
}

// Test 3: Owner closes regular 165-byte ctoken account (no compressible extension)
// Test 3: Owner cannot close regular 165-byte ctoken account (no compressible extension)
// Non-compressible accounts cannot use compress_and_close
{
let mut context = setup_account_test().await.unwrap();

Expand All @@ -76,16 +91,55 @@ async fn test_compress_and_close_owner_scenarios() {
.unwrap();
context.rpc.set_account(token_account_pubkey, token_account);

// Compress and close as owner
compress_and_close_owner_and_assert(
&mut context,
None, // Default destination (owner)
"owner_non_compressible",
let payer_pubkey = context.payer.pubkey();

// Get output queue for compression
let output_queue = context
.rpc
.get_random_state_tree_info()
.unwrap()
.get_output_pubkey()
.unwrap();

// Create compress_and_close instruction with is_compressible=false for non-compressible account
use light_token_client::instructions::transfer2::{
create_generic_transfer2_instruction, CompressAndCloseInput, Transfer2InstructionType,
};

let compress_and_close_ix = create_generic_transfer2_instruction(
&mut context.rpc,
vec![Transfer2InstructionType::CompressAndClose(
CompressAndCloseInput {
solana_ctoken_account: token_account_pubkey,
authority: context.owner_keypair.pubkey(),
output_queue,
destination: None,
is_compressible: false, // Non-compressible account
},
)],
payer_pubkey,
false,
)
.await;
.await
.unwrap();

// Execute transaction expecting failure
let result = context
.rpc
.create_and_send_transaction(
&[compress_and_close_ix],
&payer_pubkey,
&[&context.payer, &context.owner_keypair],
)
.await;

// Assert that the transaction failed with InvalidAccountData (error code 3)
// "compress and close requires compressible extension"
light_program_test::utils::assert::assert_rpc_error(result, 0, 3).unwrap();
}

// Test 4: Owner closes associated token account
// Test 4: Owner cannot close compressible associated token account
// Only compression_authority can compress and close compressible accounts
{
let mut context = setup_account_test().await.unwrap();
let payer_pubkey = context.payer.pubkey();
Expand Down Expand Up @@ -127,7 +181,6 @@ async fn test_compress_and_close_owner_scenarios() {
context.rpc.set_account(ata_pubkey, ata_account);

// Create compress_and_close instruction manually for ATA
use light_test_utils::assert_transfer2::assert_transfer2_compress_and_close;
use light_token_client::instructions::transfer2::{
create_generic_transfer2_instruction, CompressAndCloseInput, Transfer2InstructionType,
};
Expand Down Expand Up @@ -156,27 +209,18 @@ async fn test_compress_and_close_owner_scenarios() {
.await
.unwrap();

context
// Owner trying to compress and close ATA should fail with InvalidAccountData
let result = context
.rpc
.create_and_send_transaction(
&[compress_and_close_ix],
&payer_pubkey,
&[&context.payer, &context.owner_keypair],
)
.await
.unwrap();
.await;

assert_transfer2_compress_and_close(
&mut context.rpc,
CompressAndCloseInput {
solana_ctoken_account: ata_pubkey,
authority: context.owner_keypair.pubkey(),
output_queue,
destination: None,
is_compressible: true,
},
)
.await;
// Assert that the transaction failed with InvalidAccountData (error code 3)
light_program_test::utils::assert::assert_rpc_error(result, 0, 3).unwrap();
}
}

Expand Down Expand Up @@ -365,13 +409,14 @@ async fn test_compress_and_close_rent_authority_scenarios() {
#[tokio::test]
#[serial]
async fn test_compress_and_close_compress_to_pubkey() {
// Test 9: compress_to_pubkey=true, account pubkey becomes owner in compressed output (PDA use case)
// Test: compress_to_pubkey=true, account pubkey becomes owner in compressed output (PDA use case)
// Uses compression_authority (forester) since owner cannot compress and close compressible accounts
{
let mut context = setup_compress_and_close_test(
2, // 2 prepaid epochs
500, // 500 token balance
None, // No time warp needed for owner
false, // Use default rent sponsor
2, // 2 prepaid epochs
500, // 500 token balance
Some(2), // Warp to epoch 2 (makes account compressible for forester)
false, // Use default rent sponsor
)
.await
.unwrap();
Expand Down Expand Up @@ -405,11 +450,42 @@ async fn test_compress_and_close_compress_to_pubkey() {
// Write the modified account back
context.rpc.set_account(token_account_pubkey, token_account);

// Execute compress_and_close using helper
compress_and_close_owner_and_assert(
&mut context,
None, // Default destination (owner)
"compress_to_pubkey_true",
// Get forester keypair
let forester_keypair = context.rpc.test_accounts.protocol.forester.insecure_clone();

// Create destination for compression incentive
let destination = Keypair::new();
context
.rpc
.airdrop_lamports(&destination.pubkey(), 1_000_000)
.await
.unwrap();

// Compress and close using rent authority (forester)
compress_and_close_forester(
&mut context.rpc,
&[token_account_pubkey],
&forester_keypair,
&context.payer,
Some(destination.pubkey()),
)
.await
.unwrap();

// Assert compress and close succeeded - the owner in compressed output should be the token_account_pubkey
use light_test_utils::assert_transfer2::assert_transfer2_compress_and_close;
use light_token_client::instructions::transfer2::CompressAndCloseInput;

let output_queue = context.rpc.get_random_state_tree_info().unwrap().queue;
assert_transfer2_compress_and_close(
&mut context.rpc,
CompressAndCloseInput {
solana_ctoken_account: token_account_pubkey,
authority: context.compression_authority,
output_queue,
destination: Some(destination.pubkey()),
is_compressible: true,
},
)
.await;
}
Expand Down
83 changes: 0 additions & 83 deletions program-tests/compressed-token-test/tests/ctoken/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -587,89 +587,6 @@ pub async fn setup_compress_and_close_test(
Ok(context)
}

/// Compress and close account as owner and assert success
///
/// # Parameters
/// - `context`: Test context with RPC and account info
/// - `destination`: Optional destination for user funds (defaults to owner)
/// - `name`: Test name for debugging
pub async fn compress_and_close_owner_and_assert(
context: &mut AccountTestContext,
destination: Option<Pubkey>,
name: &str,
) {
use light_ctoken_types::COMPRESSIBLE_TOKEN_ACCOUNT_SIZE;
use light_test_utils::assert_transfer2::assert_transfer2_compress_and_close;
use light_token_client::instructions::transfer2::{
create_generic_transfer2_instruction, CompressAndCloseInput, Transfer2InstructionType,
};

println!("Compress and close (owner) initiated for: {}", name);

let payer_pubkey = context.payer.pubkey();
let token_account_pubkey = context.token_account_keypair.pubkey();
let owner_pubkey = context.owner_keypair.pubkey();

// Check if account is compressible by checking size
let account_info = context
.rpc
.get_account(token_account_pubkey)
.await
.unwrap()
.unwrap();
let is_compressible = account_info.data.len() == COMPRESSIBLE_TOKEN_ACCOUNT_SIZE as usize;

// Get output queue for compression
let output_queue = context
.rpc
.get_random_state_tree_info()
.unwrap()
.get_output_pubkey()
.unwrap();

// Create compress_and_close instruction as owner
let compress_and_close_ix = create_generic_transfer2_instruction(
&mut context.rpc,
vec![Transfer2InstructionType::CompressAndClose(
CompressAndCloseInput {
solana_ctoken_account: token_account_pubkey,
authority: owner_pubkey,
output_queue,
destination,
is_compressible,
},
)],
payer_pubkey,
false,
)
.await
.unwrap();

// Execute transaction
context
.rpc
.create_and_send_transaction(
&[compress_and_close_ix],
&payer_pubkey,
&[&context.payer, &context.owner_keypair],
)
.await
.unwrap();

// Assert compress and close succeeded
assert_transfer2_compress_and_close(
&mut context.rpc,
CompressAndCloseInput {
solana_ctoken_account: token_account_pubkey,
authority: owner_pubkey,
output_queue,
destination,
is_compressible,
},
)
.await;
}

/// Compress and close account expecting failure with custom authority
///
/// # Parameters
Expand Down
Loading