light_token/compressed_token/v2/
compress_and_close.rs1use light_program_profiler::profile;
2use light_sdk::{
3 error::LightSdkError,
4 instruction::{AccountMetasVec, PackedAccounts, SystemAccountMetaConfig},
5};
6use light_token_interface::{instructions::transfer2::CompressedCpiContext, state::Token};
7use light_zero_copy::traits::ZeroCopyAt;
8use solana_account_info::AccountInfo;
9use solana_cpi::invoke_signed;
10use solana_instruction::{AccountMeta, Instruction};
11use solana_msg::msg;
12use solana_pubkey::Pubkey;
13
14use super::{
15 account2::CTokenAccount2,
16 transfer2::{
17 account_metas::Transfer2AccountsMetaConfig, create_transfer2_instruction, Transfer2Config,
18 Transfer2Inputs,
19 },
20};
21use crate::{
22 error::TokenSdkError,
23 utils::{AccountInfoToCompress, TokenDefaultAccounts},
24};
25
26#[derive(Debug, Copy, Clone, crate::AnchorSerialize, crate::AnchorDeserialize)]
28pub struct CompressAndCloseIndices {
29 pub source_index: u8,
30 pub mint_index: u8,
31 pub owner_index: u8,
32 pub authority_index: u8,
33 pub rent_sponsor_index: u8,
34 pub destination_index: u8,
35}
36
37pub fn pack_for_compress_and_close(
40 ctoken_account_pubkey: Pubkey,
41 ctoken_account_data: &[u8],
42 packed_accounts: &mut PackedAccounts,
43) -> Result<CompressAndCloseIndices, TokenSdkError> {
44 let (ctoken_account, _) = Token::zero_copy_at(ctoken_account_data)?;
45 let source_index = packed_accounts.insert_or_get(ctoken_account_pubkey);
46 let mint_index = packed_accounts.insert_or_get(Pubkey::from(ctoken_account.mint.to_bytes()));
47 let owner_index = packed_accounts.insert_or_get(Pubkey::from(ctoken_account.owner.to_bytes()));
48
49 let compressible_ext = ctoken_account
51 .get_compressible_extension()
52 .ok_or(TokenSdkError::MissingCompressibleExtension)?;
53 let authority_index = packed_accounts.insert_or_get_config(
54 Pubkey::from(compressible_ext.info.compression_authority),
55 true,
56 true,
57 );
58 let rent_sponsor_index =
59 packed_accounts.insert_or_get(Pubkey::from(compressible_ext.info.rent_sponsor));
60 let destination_index = rent_sponsor_index;
62
63 Ok(CompressAndCloseIndices {
64 source_index,
65 mint_index,
66 owner_index,
67 authority_index,
68 rent_sponsor_index,
69 destination_index,
70 })
71}
72
73#[inline(always)]
75#[profile]
76fn find_account_indices(
77 find_index: impl Fn(&Pubkey) -> Option<u8>,
78 ctoken_account_key: &Pubkey,
79 mint_pubkey: &Pubkey,
80 owner_pubkey: &Pubkey,
81 authority: &Pubkey,
82 rent_sponsor_pubkey: &Pubkey,
83 destination_pubkey: &Pubkey,
84) -> Result<CompressAndCloseIndices, TokenSdkError> {
85 let source_index = find_index(ctoken_account_key).ok_or_else(|| {
86 msg!("Source ctoken account not found in packed_accounts");
87 TokenSdkError::InvalidAccountData
88 })?;
89
90 let mint_index = find_index(mint_pubkey).ok_or_else(|| {
91 msg!("Mint {} not found in packed_accounts", mint_pubkey);
92 TokenSdkError::InvalidAccountData
93 })?;
94
95 let owner_index = find_index(owner_pubkey).ok_or_else(|| {
96 msg!("Owner {} not found in packed_accounts", owner_pubkey);
97 TokenSdkError::InvalidAccountData
98 })?;
99
100 let authority_index = find_index(authority).ok_or_else(|| {
101 msg!("Authority not found in packed_accounts");
102 TokenSdkError::InvalidAccountData
103 })?;
104
105 let rent_sponsor_index = find_index(rent_sponsor_pubkey).ok_or_else(|| {
106 msg!("Rent recipient not found in packed_accounts");
107 TokenSdkError::InvalidAccountData
108 })?;
109
110 let destination_index = find_index(destination_pubkey).ok_or_else(|| {
111 msg!("Destination not found in packed_accounts");
112 TokenSdkError::InvalidAccountData
113 })?;
114
115 Ok(CompressAndCloseIndices {
116 source_index,
117 mint_index,
118 owner_index,
119 authority_index,
120 rent_sponsor_index,
121 destination_index,
122 })
123}
124
125#[profile]
136pub fn compress_and_close_token_accounts_with_indices<'info>(
137 fee_payer: Pubkey,
138 cpi_context_pubkey: Option<Pubkey>,
139 indices: &[CompressAndCloseIndices],
140 packed_accounts: &[AccountInfo<'info>],
141) -> Result<Instruction, TokenSdkError> {
142 if indices.is_empty() {
143 msg!("indices empty");
144 return Err(TokenSdkError::InvalidAccountData);
145 }
146 let mut packed_account_metas = arrayvec::ArrayVec::<AccountMeta, 32>::new();
148 for info in packed_accounts.iter() {
149 packed_account_metas.push(AccountMeta {
150 pubkey: *info.key,
151 is_signer: info.is_signer,
152 is_writable: info.is_writable,
153 });
154 }
155 let mut token_accounts = Vec::with_capacity(indices.len());
157
158 for (i, idx) in indices.iter().enumerate() {
159 let source_account = packed_accounts
161 .get(idx.source_index as usize)
162 .ok_or(TokenSdkError::InvalidAccountData)?;
163
164 let account_data = source_account
165 .try_borrow_data()
166 .map_err(|_| TokenSdkError::AccountBorrowFailed)?;
167
168 let amount = light_token_interface::state::Token::amount_from_slice(&account_data)?;
169
170 let mut token_account = CTokenAccount2::new_empty(idx.owner_index, idx.mint_index);
172
173 token_account.compress_and_close(
175 amount,
176 idx.source_index,
177 idx.authority_index,
178 idx.rent_sponsor_index,
179 i as u8, idx.destination_index, )?;
182
183 packed_account_metas[idx.authority_index as usize].is_signer = true;
185
186 token_accounts.push(token_account);
187 }
188
189 let (meta_config, transfer_config) = if let Some(cpi_context) = cpi_context_pubkey {
190 let cpi_context_config = CompressedCpiContext {
191 set_context: false,
192 first_set_context: false,
193 };
194
195 (
196 Transfer2AccountsMetaConfig {
197 fee_payer: Some(fee_payer),
198 cpi_context: Some(cpi_context),
199 decompressed_accounts_only: false,
200 sol_pool_pda: None,
201 sol_decompression_recipient: None,
202 with_sol_pool: false,
203 packed_accounts: Some(packed_account_metas.to_vec()),
204 },
205 Transfer2Config::default().with_cpi_context(cpi_context_config),
206 )
207 } else {
208 (
209 Transfer2AccountsMetaConfig::new(fee_payer, packed_account_metas.to_vec()),
210 Transfer2Config::default(),
211 )
212 };
213
214 let inputs = Transfer2Inputs {
216 meta_config,
217 token_accounts,
218 transfer_config,
219 output_queue: 0, ..Default::default()
221 };
222
223 create_transfer2_instruction(inputs)
224}
225
226#[profile]
237pub fn compress_and_close_token_accounts<'info>(
238 fee_payer: Pubkey,
239 output_queue: AccountInfo<'info>,
240 ctoken_solana_accounts: &[&AccountInfo<'info>],
241 packed_accounts: &[AccountInfo<'info>],
242) -> Result<Instruction, TokenSdkError> {
243 if ctoken_solana_accounts.is_empty() {
244 msg!("ctoken_solana_accounts empty");
245 return Err(TokenSdkError::InvalidAccountData);
246 }
247
248 let find_index = |pubkey: &Pubkey| -> Option<u8> {
252 packed_accounts
253 .iter()
254 .position(|account| account.key == pubkey)
255 .map(|idx| (idx + 1) as u8) };
257
258 let mut indices_vec = Vec::with_capacity(ctoken_solana_accounts.len());
260
261 for ctoken_account_info in ctoken_solana_accounts.iter() {
262 let account_data = ctoken_account_info
264 .try_borrow_data()
265 .map_err(|_| TokenSdkError::AccountBorrowFailed)?;
266
267 let (compressed_token, _) =
269 light_token_interface::state::Token::zero_copy_at(&account_data)
270 .map_err(|_| TokenSdkError::InvalidAccountData)?;
271
272 let mint_pubkey = Pubkey::from(compressed_token.mint.to_bytes());
274 let owner_pubkey = Pubkey::from(compressed_token.owner.to_bytes());
275
276 let compressible_ext = compressed_token
278 .get_compressible_extension()
279 .ok_or(TokenSdkError::MissingCompressibleExtension)?;
280 let authority = Pubkey::from(compressible_ext.info.compression_authority);
281 let rent_sponsor = Pubkey::from(compressible_ext.info.rent_sponsor);
282
283 let destination_pubkey = rent_sponsor;
285
286 let indices = find_account_indices(
287 find_index,
288 ctoken_account_info.key,
289 &mint_pubkey,
290 &owner_pubkey,
291 &authority,
292 &rent_sponsor,
293 &destination_pubkey,
294 )?;
295 indices_vec.push(indices);
296 }
297 let mut packed_accounts_vec = Vec::with_capacity(packed_accounts.len() + 1);
298 packed_accounts_vec.push(output_queue);
299 packed_accounts_vec.extend_from_slice(packed_accounts);
300
301 compress_and_close_token_accounts_with_indices(
302 fee_payer,
303 None,
304 &indices_vec,
305 packed_accounts_vec.as_slice(),
306 )
307}
308
309#[allow(clippy::too_many_arguments)]
318#[profile]
319#[allow(clippy::extra_unused_lifetimes)]
320pub fn compress_and_close_token_accounts_signed<'b, 'info>(
321 token_accounts_to_compress: &[AccountInfoToCompress<'info>],
322 fee_payer: AccountInfo<'info>,
323 output_queue: AccountInfo<'info>,
324 compressed_token_rent_sponsor: AccountInfo<'info>,
325 compressed_token_cpi_authority: AccountInfo<'info>,
326 cpi_authority: AccountInfo<'info>,
327 post_system: &[AccountInfo<'info>],
328 remaining_accounts: &[AccountInfo<'info>],
329) -> Result<(), TokenSdkError> {
330 let mut packed_accounts = Vec::with_capacity(post_system.len() + 4);
331 packed_accounts.extend_from_slice(post_system);
332 packed_accounts.push(cpi_authority);
333 packed_accounts.push(compressed_token_rent_sponsor.clone());
334
335 let ctoken_infos: Vec<&AccountInfo<'info>> = token_accounts_to_compress
336 .iter()
337 .map(|t| t.account_info.as_ref())
338 .collect();
339
340 let instruction = compress_and_close_token_accounts(
341 *fee_payer.key,
342 output_queue,
343 &ctoken_infos,
344 &packed_accounts,
345 )?;
346 let total_capacity = packed_accounts.len() + remaining_accounts.len() + 1;
348 let mut account_infos: Vec<AccountInfo<'info>> = Vec::with_capacity(total_capacity);
349 account_infos.extend_from_slice(&packed_accounts);
350 account_infos.push(compressed_token_cpi_authority);
351 account_infos.extend_from_slice(remaining_accounts);
352
353 let token_seeds_refs: Vec<Vec<&[u8]>> = token_accounts_to_compress
354 .iter()
355 .map(|t| t.signer_seeds.iter().map(|v| v.as_slice()).collect())
356 .collect();
357 let mut all_signer_seeds: Vec<&[&[u8]]> = Vec::with_capacity(token_seeds_refs.len());
358 for seeds in &token_seeds_refs {
359 all_signer_seeds.push(seeds.as_slice());
360 }
361
362 invoke_signed(&instruction, &account_infos, &all_signer_seeds)
363 .map_err(|e| TokenSdkError::CpiError(e.to_string()))?;
364 Ok(())
365}
366
367pub struct CompressAndCloseAccounts {
368 pub compressed_token_program: Pubkey,
369 pub cpi_authority_pda: Pubkey,
370 pub cpi_context: Option<Pubkey>,
371 pub self_program: Option<Pubkey>,
372}
373
374impl Default for CompressAndCloseAccounts {
375 fn default() -> Self {
376 Self {
377 compressed_token_program: TokenDefaultAccounts::default().compressed_token_program,
378 cpi_authority_pda: TokenDefaultAccounts::default().cpi_authority_pda,
379 cpi_context: None,
380 self_program: None,
381 }
382 }
383}
384
385impl CompressAndCloseAccounts {
386 pub fn new_with_cpi_context(cpi_context: Option<Pubkey>, self_program: Option<Pubkey>) -> Self {
387 Self {
388 compressed_token_program: TokenDefaultAccounts::default().compressed_token_program,
389 cpi_authority_pda: TokenDefaultAccounts::default().cpi_authority_pda,
390 cpi_context,
391 self_program,
392 }
393 }
394}
395
396impl AccountMetasVec for CompressAndCloseAccounts {
397 fn get_account_metas_vec(&self, accounts: &mut PackedAccounts) -> Result<(), LightSdkError> {
401 if !accounts.system_accounts_set() {
402 let mut config = SystemAccountMetaConfig::default();
403 config.self_program = self.self_program;
404 #[cfg(feature = "cpi-context")]
405 {
406 config.cpi_context = self.cpi_context;
407 }
408 #[cfg(not(feature = "cpi-context"))]
409 {
410 if self.cpi_context.is_some() {
411 msg!("Error: cpi_context is set but 'cpi-context' feature is not enabled");
412 return Err(LightSdkError::ExpectedCpiContext);
413 }
414 }
415 accounts.add_system_accounts_v2(config)?;
416 }
417 accounts.pre_accounts.extend_from_slice(&[
419 AccountMeta {
420 pubkey: self.compressed_token_program,
421 is_signer: false,
422 is_writable: false,
423 },
424 AccountMeta {
425 pubkey: self.cpi_authority_pda,
426 is_signer: false,
427 is_writable: false,
428 },
429 ]);
430 Ok(())
431 }
432}