Skip to content

Commit 5839134

Browse files
fix: compress and close hotpath (#2059)
* fix: don't require a compressed token output in compress and close hotpath when signed by owner * test: ctoken compress and close -> spl token decompress * chore: remove prints * fmt --------- Co-authored-by: Swenschaeferjohann <swen@lightprotocol.com>
1 parent 3983d75 commit 5839134

9 files changed

Lines changed: 255 additions & 31 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,163 @@ async fn test_spl_to_ctoken_transfer() {
185185

186186
println!("Successfully completed round-trip transfer: SPL -> CToken -> SPL");
187187
}
188+
189+
#[tokio::test]
190+
async fn test_ctoken_to_spl_with_compress_and_close() {
191+
use light_compressed_token_sdk::{
192+
instructions::create_ctoken_to_spl_transfer_and_close_instruction,
193+
token_pool::find_token_pool_pda_with_index,
194+
};
195+
196+
let mut rpc = LightProgramTest::new(ProgramTestConfig::new(true, None))
197+
.await
198+
.unwrap();
199+
let payer = rpc.get_payer().insecure_clone();
200+
let sender = Keypair::new();
201+
airdrop_lamports(&mut rpc, &sender.pubkey(), 1_000_000_000)
202+
.await
203+
.unwrap();
204+
let mint = create_mint_helper(&mut rpc, &payer).await;
205+
let amount = 10000u64;
206+
let transfer_amount = 5000u64;
207+
208+
// Create SPL token account and mint tokens
209+
let spl_token_account_keypair = Keypair::new();
210+
create_token_2022_account(&mut rpc, &mint, &spl_token_account_keypair, &sender, false)
211+
.await
212+
.unwrap();
213+
mint_spl_tokens(
214+
&mut rpc,
215+
&mint,
216+
&spl_token_account_keypair.pubkey(),
217+
&payer.pubkey(),
218+
&payer,
219+
amount,
220+
false,
221+
)
222+
.await
223+
.unwrap();
224+
225+
// Create recipient for compressed tokens
226+
let recipient = Keypair::new();
227+
airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000)
228+
.await
229+
.unwrap();
230+
231+
// Create compressed token ATA for recipient
232+
let instruction = light_compressed_token_sdk::instructions::create_associated_token_account(
233+
payer.pubkey(),
234+
recipient.pubkey(),
235+
mint,
236+
)
237+
.map_err(|e| RpcError::AssertRpcError(format!("Failed to create ATA instruction: {}", e)))
238+
.unwrap();
239+
rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer])
240+
.await
241+
.unwrap();
242+
let associated_token_account = derive_ctoken_ata(&recipient.pubkey(), &mint).0;
243+
244+
// Transfer SPL to CToken
245+
transfer2::spl_to_ctoken_transfer(
246+
&mut rpc,
247+
spl_token_account_keypair.pubkey(),
248+
associated_token_account,
249+
transfer_amount,
250+
&sender,
251+
&payer,
252+
)
253+
.await
254+
.unwrap();
255+
256+
// Verify compressed token balance after initial transfer
257+
{
258+
let ctoken_account_data = rpc
259+
.get_account(associated_token_account)
260+
.await
261+
.unwrap()
262+
.unwrap();
263+
let ctoken_account =
264+
spl_pod::bytemuck::pod_from_bytes::<PodAccount>(&ctoken_account_data.data[..165])
265+
.map_err(|e| {
266+
RpcError::AssertRpcError(format!("Failed to parse CToken account: {}", e))
267+
})
268+
.unwrap();
269+
assert_eq!(
270+
u64::from(ctoken_account.amount),
271+
transfer_amount,
272+
"Recipient should have {} compressed tokens",
273+
transfer_amount
274+
);
275+
}
276+
277+
// Now transfer back using CompressAndClose instead of regular transfer
278+
println!("Testing reverse transfer with CompressAndClose: ctoken to SPL");
279+
280+
// Get token pool PDA
281+
let (token_pool_pda, token_pool_pda_bump) = find_token_pool_pda_with_index(&mint, 0);
282+
283+
// Create instruction using compress_and_close variant
284+
// Note: Using spl_token::ID because create_mint_helper creates Token (not Token-2022) mints
285+
let transfer_ix = create_ctoken_to_spl_transfer_and_close_instruction(
286+
associated_token_account,
287+
spl_token_account_keypair.pubkey(),
288+
transfer_amount,
289+
recipient.pubkey(),
290+
mint,
291+
payer.pubkey(),
292+
token_pool_pda,
293+
token_pool_pda_bump,
294+
anchor_spl::token::ID,
295+
)
296+
.unwrap();
297+
298+
// Execute transaction
299+
rpc.create_and_send_transaction(&[transfer_ix], &payer.pubkey(), &[&payer, &recipient])
300+
.await
301+
.unwrap();
302+
303+
// Verify final balances
304+
{
305+
// Verify SPL token balance is restored
306+
let spl_account_data = rpc
307+
.get_account(spl_token_account_keypair.pubkey())
308+
.await
309+
.unwrap()
310+
.unwrap();
311+
let spl_account = spl_pod::bytemuck::pod_from_bytes::<PodAccount>(&spl_account_data.data)
312+
.map_err(|e| {
313+
RpcError::AssertRpcError(format!("Failed to parse SPL token account: {}", e))
314+
})
315+
.unwrap();
316+
let restored_spl_balance: u64 = spl_account.amount.into();
317+
assert_eq!(
318+
restored_spl_balance, amount,
319+
"SPL token balance should be restored to original amount"
320+
);
321+
}
322+
323+
{
324+
// Verify CToken account is CLOSED (not just balance = 0)
325+
let ctoken_account_result = rpc.get_account(associated_token_account).await.unwrap();
326+
match ctoken_account_result {
327+
None => {
328+
println!("✓ CToken account successfully closed (account does not exist)");
329+
}
330+
Some(account_data) => {
331+
assert_eq!(
332+
account_data.data.len(),
333+
0,
334+
"CToken account data should be empty after CompressAndClose"
335+
);
336+
assert_eq!(
337+
account_data.lamports, 0,
338+
"CToken account lamports should be 0 after CompressAndClose"
339+
);
340+
println!("✓ CToken account successfully closed (zeroed out)");
341+
}
342+
}
343+
}
344+
345+
println!("✓ Successfully completed CToken -> SPL transfer with CompressAndClose");
346+
println!(" This validates owner can use CompressAndClose without explicit compressed_token_account validation");
347+
}

program-tests/registry-test/tests/compressible.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use light_test_utils::{
2020
airdrop_lamports, assert_claim::assert_claim, spl::create_mint_helper, Rpc, RpcError,
2121
};
2222
use light_token_client::actions::{
23-
create_compressible_token_account, CreateCompressibleTokenAccountInputs,
23+
create_compressible_token_account, transfer_ctoken, CreateCompressibleTokenAccountInputs,
2424
};
2525
use solana_sdk::{
2626
instruction::Instruction,
@@ -1140,7 +1140,6 @@ async fn assert_not_compressible<R: Rpc>(
11401140
#[tokio::test]
11411141
async fn test_compressible_account_infinite_funding() -> Result<(), RpcError> {
11421142
use light_test_utils::assert_ctoken_transfer::assert_ctoken_transfer;
1143-
use light_token_client::actions::ctoken_transfer;
11441143

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

12831282
// Transfer all tokens from source to dest
1284-
ctoken_transfer(
1283+
transfer_ctoken(
12851284
&mut rpc,
12861285
source,
12871286
dest,

programs/compressed-token/anchor/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,10 @@ pub enum ErrorCode {
424424
MintActionInvalidCpiContextAddressTreePubkey,
425425
#[msg("CompressAndClose: Cannot use the same compressed output account for multiple closures")]
426426
CompressAndCloseDuplicateOutput,
427+
#[msg(
428+
"CompressAndClose by compression authority requires compressed token account in outputs"
429+
)]
430+
CompressAndCloseOutputMissing,
427431
}
428432

429433
impl From<ErrorCode> for ProgramError {

programs/compressed-token/program/src/transfer2/compression/ctoken/compress_and_close.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,13 @@ pub fn process_compress_and_close(
5050

5151
if compression_authority_is_signer {
5252
// Compress the complete balance to this compressed token account.
53+
let compressed_account = close_inputs
54+
.compressed_token_account
55+
.ok_or(ErrorCode::CompressAndCloseOutputMissing)?;
5356
validate_compressed_token_account(
5457
packed_accounts,
5558
amount,
56-
close_inputs.compressed_token_account,
59+
compressed_account,
5760
ctoken,
5861
compress_to_pubkey,
5962
token_account_info.key(),

programs/compressed-token/program/src/transfer2/compression/ctoken/inputs.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
use light_account_checks::packed_accounts::ProgramPackedAccounts;
2-
use light_ctoken_types::{
3-
instructions::transfer2::{
4-
ZCompressedTokenInstructionDataTransfer2, ZCompression, ZCompressionMode,
5-
ZMultiTokenTransferOutputData,
6-
},
7-
CTokenError,
2+
use light_ctoken_types::instructions::transfer2::{
3+
ZCompressedTokenInstructionDataTransfer2, ZCompression, ZCompressionMode,
4+
ZMultiTokenTransferOutputData,
85
};
96
use pinocchio::{account_info::AccountInfo, pubkey::Pubkey};
107

118
/// Compress and close specific inputs
129
pub struct CompressAndCloseInputs<'a> {
1310
pub destination: &'a AccountInfo,
1411
pub rent_sponsor: &'a AccountInfo,
15-
pub compressed_token_account: &'a ZMultiTokenTransferOutputData<'a>,
12+
pub compressed_token_account: Option<&'a ZMultiTokenTransferOutputData<'a>>,
1613
}
1714

1815
/// Input struct for ctoken compression/decompression operations
@@ -60,8 +57,7 @@ impl<'a> CTokenCompressionInputs<'a> {
6057
)?,
6158
compressed_token_account: inputs
6259
.out_token_data
63-
.get(compression.get_compressed_token_account_index()? as usize)
64-
.ok_or(CTokenError::AccountFrozen)?,
60+
.get(compression.get_compressed_token_account_index()? as usize),
6561
})
6662
} else {
6763
None

programs/compressed-token/program/src/transfer2/compression/spl.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,6 @@ fn spl_token_transfer_invoke_cpi(
7777
cpi_authority: &AccountInfo,
7878
amount: u64,
7979
) -> Result<(), ProgramError> {
80-
msg!("spl_token_transfer_invoke_cpi");
81-
msg!(
82-
"from {:?}",
83-
solana_pubkey::Pubkey::new_from_array(*from.key())
84-
);
85-
msg!("to {:?}", solana_pubkey::Pubkey::new_from_array(*to.key()));
86-
msg!("amount {:?}", amount);
8780
let bump_seed = [BUMP_CPI_AUTHORITY];
8881
let seed_array = [
8982
Seed::from(CPI_AUTHORITY_PDA_SEED),
@@ -110,13 +103,6 @@ fn spl_token_transfer_invoke(
110103
authority: &AccountInfo,
111104
amount: u64,
112105
) -> Result<(), ProgramError> {
113-
msg!("spl_token_transfer_invoke");
114-
msg!(
115-
"from {:?}",
116-
solana_pubkey::Pubkey::new_from_array(*from.key())
117-
);
118-
msg!("to {:?}", solana_pubkey::Pubkey::new_from_array(*to.key()));
119-
msg!("amount {:?}", amount);
120106
spl_token_transfer_common(program_id, from, to, authority, amount, None)
121107
}
122108

sdk-libs/compressed-token-sdk/src/instructions/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ pub use mint_to_compressed::{
5454
};
5555
pub use transfer_ctoken::{transfer_ctoken, transfer_ctoken_signed};
5656
pub use transfer_interface::{
57-
create_transfer_ctoken_to_spl_instruction, create_transfer_spl_to_ctoken_instruction,
58-
transfer_interface, transfer_interface_signed,
57+
create_ctoken_to_spl_transfer_and_close_instruction, create_transfer_ctoken_to_spl_instruction,
58+
create_transfer_spl_to_ctoken_instruction, transfer_interface, transfer_interface_signed,
5959
};
6060
pub use update_compressed_mint::{
6161
update_compressed_mint, update_compressed_mint_cpi, UpdateCompressedMintInputs,

sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,82 @@ pub fn create_transfer_ctoken_to_spl_instruction(
159159
create_transfer2_instruction(inputs)
160160
}
161161

162+
#[allow(clippy::too_many_arguments)]
163+
#[profile]
164+
pub fn create_ctoken_to_spl_transfer_and_close_instruction(
165+
source_ctoken_account: Pubkey,
166+
destination_spl_token_account: Pubkey,
167+
amount: u64,
168+
authority: Pubkey,
169+
mint: Pubkey,
170+
payer: Pubkey,
171+
token_pool_pda: Pubkey,
172+
token_pool_pda_bump: u8,
173+
spl_token_program: Pubkey,
174+
) -> Result<Instruction, TokenSdkError> {
175+
let packed_accounts = vec![
176+
// Mint (index 0)
177+
AccountMeta::new_readonly(mint, false),
178+
// Source ctoken account (index 1) - writable
179+
AccountMeta::new(source_ctoken_account, false),
180+
// Destination SPL token account (index 2) - writable
181+
AccountMeta::new(destination_spl_token_account, false),
182+
// Authority (index 3) - signer
183+
AccountMeta::new(authority, true),
184+
// Token pool PDA (index 4) - writable
185+
AccountMeta::new(token_pool_pda, false),
186+
// SPL Token program (index 5) - needed for CPI
187+
AccountMeta::new_readonly(spl_token_program, false),
188+
];
189+
190+
// First operation: compress from ctoken account to pool using compress_and_close
191+
let compress_to_pool = CTokenAccount2 {
192+
inputs: vec![],
193+
output: MultiTokenTransferOutputData::default(),
194+
compression: Some(Compression::compress_and_close_ctoken(
195+
amount, 0, // mint index
196+
1, // source ctoken account index
197+
3, // authority index
198+
0, // no rent sponsor
199+
0, // no compressed account
200+
3, // destination is authority
201+
)),
202+
delegate_is_set: false,
203+
method_used: true,
204+
};
205+
206+
// Second operation: decompress from pool to SPL token account using decompress_spl
207+
let decompress_to_spl = CTokenAccount2 {
208+
inputs: vec![],
209+
output: MultiTokenTransferOutputData::default(),
210+
compression: Some(Compression::decompress_spl(
211+
amount,
212+
0, // mint index
213+
2, // destination SPL token account index
214+
4, // pool_account_index
215+
0, // pool_index (TODO: make dynamic)
216+
token_pool_pda_bump,
217+
)),
218+
delegate_is_set: false,
219+
method_used: true,
220+
};
221+
222+
let inputs = Transfer2Inputs {
223+
validity_proof: ValidityProof::new(None),
224+
transfer_config: Transfer2Config::default().filter_zero_amount_outputs(),
225+
meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only(
226+
payer,
227+
packed_accounts,
228+
),
229+
in_lamports: None,
230+
out_lamports: None,
231+
token_accounts: vec![compress_to_pool, decompress_to_spl],
232+
output_queue: 0, // Decompressed accounts only, no output queue needed
233+
};
234+
235+
create_transfer2_instruction(inputs)
236+
}
237+
162238
/// Transfer SPL tokens to compressed tokens
163239
#[allow(clippy::too_many_arguments)]
164240
pub fn transfer_spl_to_ctoken<'info>(

0 commit comments

Comments
 (0)