Skip to main content

light_token/instruction/
decompress.rs

1use light_account::PackedAccounts;
2use light_compressed_account::instruction_data::compressed_proof::ValidityProof;
3use light_compressed_token_sdk::compressed_token::{
4    decompress_full::pack_for_decompress_full_with_ata,
5    transfer2::{
6        create_transfer2_instruction, Transfer2AccountsMetaConfig, Transfer2Config, Transfer2Inputs,
7    },
8    CTokenAccount2,
9};
10use light_sdk::instruction::PackedStateTreeInfo;
11use light_token_interface::{
12    instructions::extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData},
13    state::{AccountState, ExtensionStruct, TokenData, TokenDataVersion},
14};
15use solana_instruction::Instruction;
16use solana_program_error::ProgramError;
17use solana_pubkey::Pubkey;
18
19use crate::utils::get_associated_token_address_and_bump;
20
21/// # Decompress compressed tokens to a cToken account
22///
23/// ```rust
24/// # use solana_pubkey::Pubkey;
25/// # use light_token::instruction::Decompress;
26/// # use light_token::compat::TokenData;
27/// # use light_compressed_account::instruction_data::compressed_proof::ValidityProof;
28/// # let destination = Pubkey::new_unique();
29/// # let payer = Pubkey::new_unique();
30/// # let signer = Pubkey::new_unique();
31/// # let merkle_tree = Pubkey::new_unique();
32/// # let queue = Pubkey::new_unique();
33/// # let token_data = TokenData::default();
34/// # let discriminator = [0, 0, 0, 0, 0, 0, 0, 4]; // ShaFlat
35/// let instruction = Decompress {
36///     token_data: token_data.into(),
37///     discriminator,
38///     merkle_tree,
39///     queue,
40///     leaf_index: 0,
41///     root_index: 0,
42///     destination,
43///     payer,
44///     signer,
45///     validity_proof: ValidityProof::new(None),
46/// }.instruction()?;
47/// # Ok::<(), solana_program_error::ProgramError>(())
48/// ```
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Decompress {
51    /// Token data from the compressed account (compat version with solana_pubkey::Pubkey)
52    pub token_data: TokenData,
53    /// Compressed Token Account discriminator
54    pub discriminator: [u8; 8],
55    /// Merkle tree pubkey
56    pub merkle_tree: Pubkey,
57    /// Queue pubkey
58    pub queue: Pubkey,
59    /// Leaf index in the Merkle tree
60    pub leaf_index: u32,
61    /// Root index
62    pub root_index: u16,
63    /// Destination cToken account (must exist)
64    pub destination: Pubkey,
65    /// Fee payer
66    pub payer: Pubkey,
67    /// Signer (wallet owner, delegate, or permanent delegate)
68    pub signer: Pubkey,
69    /// Validity proof for the compressed account
70    pub validity_proof: ValidityProof,
71}
72
73impl Decompress {
74    pub fn instruction(self) -> Result<Instruction, ProgramError> {
75        // Build packed accounts
76        // Note: Don't add system accounts here - Transfer2AccountsMetaConfig adds them
77        let mut packed_accounts = PackedAccounts::default();
78
79        // Insert merkle tree and queue to get their indices
80        let merkle_tree_pubkey_index = packed_accounts.insert_or_get(self.merkle_tree);
81        let queue_pubkey_index = packed_accounts.insert_or_get(self.queue);
82
83        // Build PackedStateTreeInfo
84        // prove_by_index is true if validity proof is None (no ZK proof)
85        let prove_by_index = self.validity_proof.0.is_none();
86        let tree_info = PackedStateTreeInfo {
87            merkle_tree_pubkey_index,
88            queue_pubkey_index,
89            leaf_index: self.leaf_index,
90            root_index: self.root_index,
91            prove_by_index,
92        };
93        // Extract version from discriminator
94        let version = TokenDataVersion::from_discriminator(self.discriminator)
95            .map_err(|_| ProgramError::InvalidAccountData)? as u8;
96
97        // Check if this is an ATA decompress (is_ata flag in stored TLV)
98        let is_ata = self.token_data.tlv.as_ref().is_some_and(|exts| {
99            exts.iter()
100                .any(|e| matches!(e, ExtensionStruct::CompressedOnly(co) if co.is_ata != 0))
101        });
102
103        // For ATA decompress, derive the bump from wallet owner + mint
104        // The signer is the wallet owner for ATAs
105        let ata_bump = if is_ata {
106            let (_, bump) = get_associated_token_address_and_bump(
107                &self.signer,
108                &Pubkey::from(self.token_data.mint.to_bytes()),
109            );
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 as u8;
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        let amount: u64 = self.token_data.amount;
146        let indices = pack_for_decompress_full_with_ata(
147            &self.token_data.into(),
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(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}