light_token/compressed_token/v2/
decompress_full.rs

1use light_compressed_account::compressed_account::PackedMerkleContext;
2use light_program_profiler::profile;
3use light_sdk::{
4    error::LightSdkError,
5    instruction::{AccountMetasVec, PackedAccounts, PackedStateTreeInfo, SystemAccountMetaConfig},
6};
7use light_token_interface::instructions::{
8    extensions::ExtensionInstructionData,
9    transfer2::{CompressedCpiContext, MultiInputTokenDataWithContext},
10};
11use solana_account_info::AccountInfo;
12use solana_instruction::{AccountMeta, Instruction};
13use solana_pubkey::Pubkey;
14
15use super::{
16    account2::CTokenAccount2,
17    transfer2::{
18        account_metas::Transfer2AccountsMetaConfig, create_transfer2_instruction, Transfer2Config,
19        Transfer2Inputs,
20    },
21};
22use crate::{
23    compat::TokenData, error::TokenSdkError, utils::TokenDefaultAccounts, AnchorDeserialize,
24    AnchorSerialize, ValidityProof,
25};
26
27/// Struct to hold all the data needed for DecompressFull operation
28/// Contains the complete compressed account data and destination index
29#[derive(Debug, Clone, AnchorDeserialize, AnchorSerialize)]
30pub struct DecompressFullIndices {
31    pub source: MultiInputTokenDataWithContext, // Complete compressed account data with merkle context
32    pub destination_index: u8,                  // Destination ctoken Solana account (must exist)
33    /// TLV extensions for this compressed account (e.g., CompressedOnly extension).
34    /// Used to transfer extension state during decompress.
35    pub tlv: Option<Vec<ExtensionInstructionData>>,
36    /// Whether this is an ATA decompression. For ATAs, the source.owner is the ATA address
37    /// (not the wallet), so it should NOT be marked as a signer - the wallet signs the tx instead.
38    pub is_ata: bool,
39}
40
41/// Decompress full balance from compressed token accounts with pre-computed indices
42///
43/// # Arguments
44/// * `fee_payer` - The fee payer pubkey
45/// * `validity_proof` - Validity proof for the compressed accounts (zkp or index)
46/// * `cpi_context_pubkey` - Optional CPI context account for optimized multi-program transactions
47/// * `indices` - Slice of source/destination pairs for decompress operations
48/// * `packed_accounts` - Slice of all accounts that will be used in the instruction
49///
50/// # Returns
51/// An instruction that decompresses the full balance of all provided token accounts
52#[profile]
53pub fn decompress_full_token_accounts_with_indices<'info>(
54    fee_payer: Pubkey,
55    validity_proof: ValidityProof,
56    cpi_context_pubkey: Option<Pubkey>,
57    indices: &[DecompressFullIndices],
58    packed_accounts: &[AccountInfo<'info>],
59) -> Result<Instruction, TokenSdkError> {
60    if indices.is_empty() {
61        return Err(TokenSdkError::InvalidAccountData);
62    }
63
64    // Process each set of indices
65    let mut token_accounts = Vec::with_capacity(indices.len());
66    let mut in_tlv_data: Vec<Vec<ExtensionInstructionData>> = Vec::with_capacity(indices.len());
67    let mut has_any_tlv = false;
68
69    // Convert packed_accounts to AccountMetas
70    // TODO: we may have to add conditional delegate signers for delegate
71    // support via CPI.
72    // Build signer flags in O(n) instead of scanning on every meta push
73    let mut signer_flags = vec![false; packed_accounts.len()];
74
75    for idx in indices.iter() {
76        // Create CTokenAccount2 with the source data
77        // For decompress_full, we don't have an output tree since everything goes to the destination
78        let mut token_account = CTokenAccount2::new(vec![idx.source])?;
79
80        // Set up decompress_full - decompress entire balance to destination ctoken account
81        token_account.decompress(idx.source.amount, idx.destination_index)?;
82        token_accounts.push(token_account);
83
84        // Collect TLV data for this input
85        if let Some(tlv) = &idx.tlv {
86            has_any_tlv = true;
87            in_tlv_data.push(tlv.clone());
88        } else {
89            in_tlv_data.push(Vec::new());
90        }
91
92        let owner_idx = idx.source.owner as usize;
93        if owner_idx >= signer_flags.len() {
94            return Err(TokenSdkError::InvalidAccountData);
95        }
96        // For ATAs, the owner is the ATA address (a PDA that can't sign).
97        // The wallet signs the transaction instead, so don't mark the owner as signer.
98        if !idx.is_ata {
99            signer_flags[owner_idx] = true;
100        }
101    }
102
103    let mut packed_account_metas = Vec::with_capacity(packed_accounts.len());
104
105    for (i, info) in packed_accounts.iter().enumerate() {
106        packed_account_metas.push(AccountMeta {
107            pubkey: *info.key,
108            is_signer: info.is_signer || signer_flags[i],
109            is_writable: info.is_writable,
110        });
111    }
112    let (meta_config, transfer_config) = if let Some(cpi_context) = cpi_context_pubkey {
113        let cpi_context_config = CompressedCpiContext {
114            set_context: false,
115            first_set_context: false,
116        };
117
118        (
119            Transfer2AccountsMetaConfig {
120                fee_payer: Some(fee_payer),
121                cpi_context: Some(cpi_context),
122                decompressed_accounts_only: false,
123                sol_pool_pda: None,
124                sol_decompression_recipient: None,
125                with_sol_pool: false,
126                packed_accounts: Some(packed_account_metas),
127            },
128            Transfer2Config::default()
129                .filter_zero_amount_outputs()
130                .with_cpi_context(cpi_context_config),
131        )
132    } else {
133        (
134            Transfer2AccountsMetaConfig::new(fee_payer, packed_account_metas),
135            Transfer2Config::default().filter_zero_amount_outputs(),
136        )
137    };
138
139    // Create the transfer2 instruction with all decompress operations
140    let inputs = Transfer2Inputs {
141        meta_config,
142        token_accounts,
143        transfer_config,
144        validity_proof,
145        in_tlv: if has_any_tlv { Some(in_tlv_data) } else { None },
146        ..Default::default()
147    };
148
149    create_transfer2_instruction(inputs)
150}
151
152/// Helper function to pack compressed token accounts into DecompressFullIndices
153/// Used in tests to build indices for multiple compressed accounts to decompress
154///
155/// # Arguments
156/// * `token_data` - Slice of TokenData from compressed accounts
157/// * `tree_infos` - Packed tree info for each compressed account
158/// * `destination_indices` - Destination account indices for each decompression
159/// * `packed_accounts` - PackedAccounts that will be used to insert/get indices
160/// * `tlv` - Optional TLV extensions for the compressed account
161/// * `version` - TokenDataVersion (1=V1, 2=V2, 3=ShaFlat) for hash computation
162///
163/// # Returns
164/// Vec of DecompressFullIndices ready to use with decompress_full_token_accounts_with_indices
165#[profile]
166pub fn pack_for_decompress_full(
167    token: &TokenData,
168    tree_info: &PackedStateTreeInfo,
169    destination: Pubkey,
170    packed_accounts: &mut PackedAccounts,
171    tlv: Option<Vec<ExtensionInstructionData>>,
172    version: u8,
173) -> DecompressFullIndices {
174    let source = MultiInputTokenDataWithContext {
175        owner: packed_accounts.insert_or_get_config(token.owner, true, false),
176        amount: token.amount,
177        has_delegate: token.delegate.is_some(),
178        delegate: token
179            .delegate
180            .map(|d| packed_accounts.insert_or_get(d))
181            .unwrap_or(0),
182        mint: packed_accounts.insert_or_get(token.mint),
183        version,
184        merkle_context: PackedMerkleContext {
185            merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
186            queue_pubkey_index: tree_info.queue_pubkey_index,
187            prove_by_index: tree_info.prove_by_index,
188            leaf_index: tree_info.leaf_index,
189        },
190        root_index: tree_info.root_index,
191    };
192
193    DecompressFullIndices {
194        source,
195        destination_index: packed_accounts.insert_or_get(destination),
196        tlv,
197        is_ata: false, // Non-ATA: owner is a signer
198    }
199}
200
201/// Pack accounts for decompress with ATA support.
202///
203/// For ATA decompress (is_ata=true):
204/// - Owner (ATA pubkey) is added without signer flag (ATA can't sign)
205/// - Wallet owner is already added as signer by the caller
206///
207/// For non-ATA decompress:
208/// - Owner is added as signer (normal case)
209#[profile]
210pub fn pack_for_decompress_full_with_ata(
211    token: &TokenData,
212    tree_info: &PackedStateTreeInfo,
213    destination: Pubkey,
214    packed_accounts: &mut PackedAccounts,
215    tlv: Option<Vec<ExtensionInstructionData>>,
216    version: u8,
217    is_ata: bool,
218) -> DecompressFullIndices {
219    // For ATA: owner (ATA pubkey) is not a signer - wallet owner signs instead
220    // For non-ATA: owner is a signer
221    let owner_is_signer = !is_ata;
222
223    let source = MultiInputTokenDataWithContext {
224        owner: packed_accounts.insert_or_get_config(token.owner, owner_is_signer, false),
225        amount: token.amount,
226        has_delegate: token.delegate.is_some(),
227        delegate: token
228            .delegate
229            .map(|d| packed_accounts.insert_or_get(d))
230            .unwrap_or(0),
231        mint: packed_accounts.insert_or_get(token.mint),
232        version,
233        merkle_context: PackedMerkleContext {
234            merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
235            queue_pubkey_index: tree_info.queue_pubkey_index,
236            prove_by_index: tree_info.prove_by_index,
237            leaf_index: tree_info.leaf_index,
238        },
239        root_index: tree_info.root_index,
240    };
241
242    DecompressFullIndices {
243        source,
244        destination_index: packed_accounts.insert_or_get(destination),
245        tlv,
246        is_ata,
247    }
248}
249
250pub struct DecompressFullAccounts {
251    pub compressed_token_program: Pubkey,
252    pub cpi_authority_pda: Pubkey,
253    pub cpi_context: Option<Pubkey>,
254    pub self_program: Option<Pubkey>,
255}
256
257impl DecompressFullAccounts {
258    pub fn new(cpi_context: Option<Pubkey>) -> Self {
259        Self {
260            compressed_token_program: TokenDefaultAccounts::default().compressed_token_program,
261            cpi_authority_pda: TokenDefaultAccounts::default().cpi_authority_pda,
262            cpi_context,
263            self_program: None,
264        }
265    }
266    pub fn new_with_cpi_context(cpi_context: Option<Pubkey>, self_program: Option<Pubkey>) -> Self {
267        Self {
268            compressed_token_program: TokenDefaultAccounts::default().compressed_token_program,
269            cpi_authority_pda: TokenDefaultAccounts::default().cpi_authority_pda,
270            cpi_context,
271            self_program,
272        }
273    }
274}
275
276impl AccountMetasVec for DecompressFullAccounts {
277    /// Adds:
278    /// 1. system accounts if not set
279    /// 2. compressed token program and ctoken cpi authority pda to pre accounts
280    fn get_account_metas_vec(&self, accounts: &mut PackedAccounts) -> Result<(), LightSdkError> {
281        if !accounts.system_accounts_set() {
282            #[cfg(feature = "cpi-context")]
283            let config = {
284                let mut config = SystemAccountMetaConfig::default();
285                config.self_program = self.self_program;
286                config.cpi_context = self.cpi_context;
287                config
288            };
289            #[cfg(not(feature = "cpi-context"))]
290            let config = {
291                let mut config = SystemAccountMetaConfig::default();
292                config.self_program = self.self_program;
293                config
294            };
295
296            accounts.add_system_accounts_v2(config)?;
297        }
298        // Add both accounts in one operation for better performance
299        accounts.pre_accounts.extend_from_slice(&[
300            AccountMeta {
301                pubkey: self.compressed_token_program,
302                is_signer: false,
303                is_writable: false,
304            },
305            AccountMeta {
306                pubkey: self.cpi_authority_pda,
307                is_signer: false,
308                is_writable: false,
309            },
310        ]);
311        Ok(())
312    }
313}