light_token/compressed_token/v2/
compress_and_close.rs

1use 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/// Struct to hold all the indices needed for CompressAndClose operation
27#[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
37/// Use in the client not in solana program.
38///
39pub 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    // Get compression info from Compressible extension
50    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    // When compression authority closes, everything goes to rent sponsor
61    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/// Find and validate all required account indices from packed_accounts
74#[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/// Compress and close compressed token accounts with pre-computed indices
126///
127/// # Arguments
128/// * `fee_payer` - The fee payer pubkey
129/// * `cpi_context_pubkey` - Optional CPI context account for optimized multi-program transactions
130/// * `indices` - Slice of pre-computed indices for each account to compress and close
131/// * `packed_accounts` - Slice of all accounts that will be used in the instruction (tree accounts)
132///
133/// # Returns
134/// An instruction that compresses and closes all provided token accounts
135#[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    // Convert packed_accounts to AccountMetas using ArrayVec to avoid heap allocation
147    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    // Process each set of indices
156    let mut token_accounts = Vec::with_capacity(indices.len());
157
158    for (i, idx) in indices.iter().enumerate() {
159        // Get the amount from the source token account
160        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        // Create CTokenAccount2 for CompressAndClose operation
171        let mut token_account = CTokenAccount2::new_empty(idx.owner_index, idx.mint_index);
172
173        // Set up compress_and_close with actual indices
174        token_account.compress_and_close(
175            amount,
176            idx.source_index,
177            idx.authority_index,
178            idx.rent_sponsor_index,
179            i as u8,               // Pass the index in the output array
180            idx.destination_index, // destination for user funds
181        )?;
182
183        // Compression authority must sign
184        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    // Create the transfer2 instruction with all CompressAndClose operations
215    let inputs = Transfer2Inputs {
216        meta_config,
217        token_accounts,
218        transfer_config,
219        output_queue: 0, // Output queue is at index 0 in packed_accounts
220        ..Default::default()
221    };
222
223    create_transfer2_instruction(inputs)
224}
225
226/// Compress and close compressed token accounts
227///
228/// # Arguments
229/// * `fee_payer` - The fee payer pubkey
230/// * `output_queue_pubkey` - The output queue pubkey where compressed accounts will be stored
231/// * `ctoken_solana_accounts` - Slice of ctoken Solana account infos to compress and close
232/// * `packed_accounts` - Slice of all accounts that will be used in the instruction (tree accounts)
233///
234/// # Returns
235/// An instruction that compresses and closes all provided token accounts
236#[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    // Helper function to find index of a pubkey in packed_accounts using linear search
249    // More efficient than HashMap for small arrays in Solana programs
250    // Note: We add 1 to account for output_queue being inserted at index 0 later
251    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) // Add 1 because output_queue will be at index 0
256    };
257
258    // Process each ctoken Solana account and build indices
259    let mut indices_vec = Vec::with_capacity(ctoken_solana_accounts.len());
260
261    for ctoken_account_info in ctoken_solana_accounts.iter() {
262        // Deserialize the ctoken Solana account using light zero copy
263        let account_data = ctoken_account_info
264            .try_borrow_data()
265            .map_err(|_| TokenSdkError::AccountBorrowFailed)?;
266
267        // Deserialize the full Token including extensions
268        let (compressed_token, _) =
269            light_token_interface::state::Token::zero_copy_at(&account_data)
270                .map_err(|_| TokenSdkError::InvalidAccountData)?;
271
272        // Extract pubkeys from the deserialized account
273        let mint_pubkey = Pubkey::from(compressed_token.mint.to_bytes());
274        let owner_pubkey = Pubkey::from(compressed_token.owner.to_bytes());
275
276        // Get compression info from Compressible extension
277        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        // When compression authority closes, everything goes to rent sponsor
284        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/// Compress and close ctoken accounts, and invoke cpi.
310///
311/// Wraps `compress_and_close_token_accounts`, builds the instruction, and
312/// calls `invoke_signed` with provided seeds.
313///
314/// `remaining_accounts` must include required Light system accounts for
315/// `transfer2`, followed by any additional accounts. Post_system accounts are a
316/// subset of `remaining_accounts`.
317#[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    // infos
347    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    /// Adds:
398    /// 1. system accounts if not set
399    /// 2. compressed token program and ctoken cpi authority pda to pre accounts
400    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        // Add both accounts in one operation for better performance
418        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}