light_token/instruction/
decompress.rs

1use light_compressed_account::instruction_data::compressed_proof::ValidityProof;
2use light_sdk::instruction::{PackedAccounts, PackedStateTreeInfo};
3use light_token_interface::{
4    instructions::extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData},
5    state::{ExtensionStruct, TokenDataVersion},
6};
7use solana_instruction::Instruction;
8use solana_program_error::ProgramError;
9use solana_pubkey::Pubkey;
10
11use crate::{
12    compat::{AccountState, TokenData},
13    compressed_token::{
14        decompress_full::pack_for_decompress_full_with_ata,
15        transfer2::{
16            create_transfer2_instruction, Transfer2AccountsMetaConfig, Transfer2Config,
17            Transfer2Inputs,
18        },
19        CTokenAccount2,
20    },
21    instruction::derive_associated_token_account,
22};
23
24/// # Decompress compressed tokens to a cToken account
25///
26/// ```rust
27/// # use solana_pubkey::Pubkey;
28/// # use light_token::instruction::Decompress;
29/// # use light_token::compat::TokenData;
30/// # use light_compressed_account::instruction_data::compressed_proof::ValidityProof;
31/// # let destination = Pubkey::new_unique();
32/// # let payer = Pubkey::new_unique();
33/// # let signer = Pubkey::new_unique();
34/// # let merkle_tree = Pubkey::new_unique();
35/// # let queue = Pubkey::new_unique();
36/// # let token_data = TokenData::default();
37/// # let discriminator = [0, 0, 0, 0, 0, 0, 0, 4]; // ShaFlat
38/// let instruction = Decompress {
39///     token_data,
40///     discriminator,
41///     merkle_tree,
42///     queue,
43///     leaf_index: 0,
44///     root_index: 0,
45///     destination,
46///     payer,
47///     signer,
48///     validity_proof: ValidityProof::new(None),
49/// }.instruction()?;
50/// # Ok::<(), solana_program_error::ProgramError>(())
51/// ```
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct Decompress {
54    /// Token data from the compressed account (compat version with solana_pubkey::Pubkey)
55    pub token_data: TokenData,
56    /// Compressed Token Account discriminator
57    pub discriminator: [u8; 8],
58    /// Merkle tree pubkey
59    pub merkle_tree: Pubkey,
60    /// Queue pubkey
61    pub queue: Pubkey,
62    /// Leaf index in the Merkle tree
63    pub leaf_index: u32,
64    /// Root index
65    pub root_index: u16,
66    /// Destination cToken account (must exist)
67    pub destination: Pubkey,
68    /// Fee payer
69    pub payer: Pubkey,
70    /// Signer (wallet owner, delegate, or permanent delegate)
71    pub signer: Pubkey,
72    /// Validity proof for the compressed account
73    pub validity_proof: ValidityProof,
74}
75
76impl Decompress {
77    pub fn instruction(self) -> Result<Instruction, ProgramError> {
78        // Build packed accounts
79        // Note: Don't add system accounts here - Transfer2AccountsMetaConfig adds them
80        let mut packed_accounts = PackedAccounts::default();
81
82        // Insert merkle tree and queue to get their indices
83        let merkle_tree_pubkey_index = packed_accounts.insert_or_get(self.merkle_tree);
84        let queue_pubkey_index = packed_accounts.insert_or_get(self.queue);
85
86        // Build PackedStateTreeInfo
87        // prove_by_index is true if validity proof is None (no ZK proof)
88        let prove_by_index = self.validity_proof.0.is_none();
89        let tree_info = PackedStateTreeInfo {
90            merkle_tree_pubkey_index,
91            queue_pubkey_index,
92            leaf_index: self.leaf_index,
93            root_index: self.root_index,
94            prove_by_index,
95        };
96        // Extract version from discriminator
97        let version = TokenDataVersion::from_discriminator(self.discriminator)
98            .map_err(|_| ProgramError::InvalidAccountData)? as u8;
99
100        // Check if this is an ATA decompress (is_ata flag in stored TLV)
101        let is_ata = self.token_data.tlv.as_ref().is_some_and(|exts| {
102            exts.iter()
103                .any(|e| matches!(e, ExtensionStruct::CompressedOnly(co) if co.is_ata != 0))
104        });
105
106        // For ATA decompress, derive the bump from wallet owner + mint
107        // The signer is the wallet owner for ATAs
108        let ata_bump = if is_ata {
109            let (_, bump) = derive_associated_token_account(&self.signer, &self.token_data.mint);
110            bump
111        } else {
112            0
113        };
114
115        // Insert signer (wallet owner, delegate, or permanent delegate) as a signer account
116        let owner_index = packed_accounts.insert_or_get_config(self.signer, true, false);
117
118        // Convert TLV extensions from state format to instruction format
119        let is_frozen = self.token_data.state == AccountState::Frozen;
120        let tlv: Option<Vec<ExtensionInstructionData>> =
121            self.token_data.tlv.as_ref().map(|extensions| {
122                extensions
123                    .iter()
124                    .filter_map(|ext| match ext {
125                        ExtensionStruct::CompressedOnly(compressed_only) => {
126                            Some(ExtensionInstructionData::CompressedOnly(
127                                CompressedOnlyExtensionInstructionData {
128                                    delegated_amount: compressed_only.delegated_amount,
129                                    withheld_transfer_fee: compressed_only.withheld_transfer_fee,
130                                    is_frozen,
131                                    compression_index: 0,
132                                    is_ata: compressed_only.is_ata != 0,
133                                    bump: ata_bump,
134                                    owner_index,
135                                },
136                            ))
137                        }
138                        _ => None,
139                    })
140                    .collect()
141            });
142
143        // Clone tlv for passing to Transfer2Inputs.in_tlv
144        let in_tlv = tlv.clone().map(|t| vec![t]);
145
146        let indices = pack_for_decompress_full_with_ata(
147            &self.token_data,
148            &tree_info,
149            self.destination,
150            &mut packed_accounts,
151            tlv,
152            version,
153            is_ata,
154        );
155        // Build CTokenAccount2 with decompress operation
156        let mut token_account = CTokenAccount2::new(vec![indices.source])
157            .map_err(|_| ProgramError::InvalidAccountData)?;
158        token_account
159            .decompress(self.token_data.amount, indices.destination_index)
160            .map_err(|_| ProgramError::InvalidAccountData)?;
161
162        // Build instruction inputs
163        let (packed_account_metas, _, _) = packed_accounts.to_account_metas();
164        let meta_config = Transfer2AccountsMetaConfig::new(self.payer, packed_account_metas);
165        let transfer_config = Transfer2Config::default().filter_zero_amount_outputs();
166
167        let inputs = Transfer2Inputs {
168            meta_config,
169            token_accounts: vec![token_account],
170            transfer_config,
171            validity_proof: self.validity_proof,
172            in_tlv,
173            ..Default::default()
174        };
175
176        create_transfer2_instruction(inputs).map_err(ProgramError::from)
177    }
178}