light_token/instruction/
decompress_mint.rs

1use light_compressed_account::instruction_data::{
2    compressed_proof::ValidityProof, traits::LightInstructionData,
3};
4use light_token_interface::instructions::mint_action::{
5    CpiContext, DecompressMintAction, MintActionCompressedInstructionData, MintWithContext,
6};
7use solana_account_info::AccountInfo;
8use solana_cpi::{invoke, invoke_signed};
9use solana_instruction::Instruction;
10use solana_program_error::ProgramError;
11use solana_pubkey::Pubkey;
12
13use crate::{
14    compressed_token::mint_action::MintActionMetaConfig,
15    instruction::{config_pda, rent_sponsor_pda, SystemAccountInfos},
16};
17
18/// Decompress a compressed mint to a Mint Solana account.
19///
20/// Creates an on-chain Mint PDA that becomes the source of truth.
21/// The Mint is always compressible.
22///
23/// # Example
24/// ```rust,ignore
25/// let instruction = DecompressMint {
26///     payer,
27///     authority,
28///     state_tree,
29///     input_queue,
30///     output_queue,
31///     compressed_mint_with_context,
32///     proof,
33///     rent_payment: 16,       // epochs (~24 hours rent)
34///     write_top_up: 766,      // lamports (~3 hours rent per write)
35/// }.instruction()?;
36/// ```
37#[derive(Debug, Clone)]
38pub struct DecompressMint {
39    /// Fee payer
40    pub payer: Pubkey,
41    /// Mint authority (must sign)
42    pub authority: Pubkey,
43    /// State tree for the compressed mint
44    pub state_tree: Pubkey,
45    /// Input queue for reading compressed mint
46    pub input_queue: Pubkey,
47    /// Output queue for updated compressed mint
48    pub output_queue: Pubkey,
49    /// Compressed mint with context (from indexer)
50    pub compressed_mint_with_context: MintWithContext,
51    /// Validity proof for the compressed mint
52    pub proof: ValidityProof,
53    /// Rent payment in epochs (must be >= 2)
54    pub rent_payment: u8,
55    /// Lamports for future write operations
56    pub write_top_up: u32,
57}
58
59impl DecompressMint {
60    pub fn instruction(self) -> Result<Instruction, ProgramError> {
61        // Get Mint PDA from compressed mint metadata
62        let mint_data = self
63            .compressed_mint_with_context
64            .mint
65            .as_ref()
66            .ok_or(ProgramError::InvalidInstructionData)?;
67        let mint_pda = Pubkey::from(mint_data.metadata.mint.to_bytes());
68
69        // Build DecompressMintAction
70        let action = DecompressMintAction {
71            rent_payment: self.rent_payment,
72            write_top_up: self.write_top_up,
73        };
74
75        // Build instruction data
76        let instruction_data = MintActionCompressedInstructionData::new(
77            self.compressed_mint_with_context,
78            self.proof.0,
79        )
80        .with_decompress_mint(action);
81
82        // Build account metas with compressible Mint
83        let meta_config = MintActionMetaConfig::new(
84            self.payer,
85            self.authority,
86            self.state_tree,
87            self.input_queue,
88            self.output_queue,
89        )
90        .with_compressible_mint(mint_pda, config_pda(), rent_sponsor_pda());
91
92        let account_metas = meta_config.to_account_metas();
93
94        let data = instruction_data
95            .data()
96            .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
97
98        Ok(Instruction {
99            program_id: Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID),
100            accounts: account_metas,
101            data,
102        })
103    }
104}
105
106// ============================================================================
107// CPI Struct: DecompressMintCpi
108// ============================================================================
109
110/// Decompress a compressed mint to a Mint Solana account via CPI.
111///
112/// Creates an on-chain Mint PDA that becomes the source of truth.
113/// The Mint is always compressible.
114///
115/// # Example
116/// ```rust,ignore
117/// DecompressMintCpi {
118///     authority: authority_account,
119///     payer: payer_account,
120///     mint: mint_account,
121///     compressible_config: config_account,
122///     rent_sponsor: rent_sponsor_account,
123///     state_tree: state_tree_account,
124///     input_queue: input_queue_account,
125///     output_queue: output_queue_account,
126///     system_accounts,
127///     compressed_mint_with_context,
128///     proof,
129///     rent_payment: 16,
130///     write_top_up: 766,
131/// }
132/// .invoke()?;
133/// ```
134pub struct DecompressMintCpi<'info> {
135    /// Mint authority (must sign)
136    pub authority: AccountInfo<'info>,
137    /// Fee payer
138    pub payer: AccountInfo<'info>,
139    /// Mint PDA account (writable)
140    pub mint: AccountInfo<'info>,
141    /// CompressibleConfig account
142    pub compressible_config: AccountInfo<'info>,
143    /// Rent sponsor PDA account
144    pub rent_sponsor: AccountInfo<'info>,
145    /// State tree for the compressed mint
146    pub state_tree: AccountInfo<'info>,
147    /// Input queue for reading compressed mint
148    pub input_queue: AccountInfo<'info>,
149    /// Output queue for updated compressed mint
150    pub output_queue: AccountInfo<'info>,
151    /// System accounts for Light Protocol
152    pub system_accounts: SystemAccountInfos<'info>,
153    /// Compressed mint with context (from indexer)
154    pub compressed_mint_with_context: MintWithContext,
155    /// Validity proof for the compressed mint
156    pub proof: ValidityProof,
157    /// Rent payment in epochs (must be >= 2)
158    pub rent_payment: u8,
159    /// Lamports for future write operations
160    pub write_top_up: u32,
161}
162
163impl<'info> DecompressMintCpi<'info> {
164    pub fn instruction(&self) -> Result<Instruction, ProgramError> {
165        DecompressMint::try_from(self)?.instruction()
166    }
167
168    pub fn invoke(self) -> Result<(), ProgramError> {
169        let instruction = self.instruction()?;
170
171        // Account order must match to_account_metas() from MintActionMetaConfig:
172        // 1. light_system_program
173        // 2. mint_signer (no sign for decompress)
174        // 3. authority (signer)
175        // 4. compressible_config
176        // 5. mint
177        // 6. rent_sponsor
178        // 7. fee_payer (signer)
179        // 8. cpi_authority_pda
180        // 9. registered_program_pda
181        // 10. account_compression_authority
182        // 11. account_compression_program
183        // 12. system_program
184        // 13. output_queue
185        // 14. tree_pubkey (state_tree)
186        // 15. input_queue
187        let account_infos = self.build_account_infos();
188        invoke(&instruction, &account_infos)
189    }
190
191    pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
192        let instruction = self.instruction()?;
193        let account_infos = self.build_account_infos();
194        invoke_signed(&instruction, &account_infos, signer_seeds)
195    }
196
197    fn build_account_infos(&self) -> Vec<AccountInfo<'info>> {
198        vec![
199            self.system_accounts.light_system_program.clone(),
200            self.authority.clone(),
201            self.compressible_config.clone(),
202            self.mint.clone(),
203            self.rent_sponsor.clone(),
204            self.payer.clone(),
205            self.system_accounts.cpi_authority_pda.clone(),
206            self.system_accounts.registered_program_pda.clone(),
207            self.system_accounts.account_compression_authority.clone(),
208            self.system_accounts.account_compression_program.clone(),
209            self.system_accounts.system_program.clone(),
210            self.output_queue.clone(),
211            self.state_tree.clone(),
212            self.input_queue.clone(),
213        ]
214    }
215}
216
217impl<'info> TryFrom<&DecompressMintCpi<'info>> for DecompressMint {
218    type Error = ProgramError;
219
220    fn try_from(cpi: &DecompressMintCpi<'info>) -> Result<Self, Self::Error> {
221        Ok(Self {
222            payer: *cpi.payer.key,
223            authority: *cpi.authority.key,
224            state_tree: *cpi.state_tree.key,
225            input_queue: *cpi.input_queue.key,
226            output_queue: *cpi.output_queue.key,
227            compressed_mint_with_context: cpi.compressed_mint_with_context.clone(),
228            proof: cpi.proof,
229            rent_payment: cpi.rent_payment,
230            write_top_up: cpi.write_top_up,
231        })
232    }
233}
234
235/// Decompress a compressed mint with CPI context support.
236///
237/// For use in multi-operation ixns where mints are decompressed
238/// along with PDAs and token accounts using a single proof.
239#[derive(Debug, Clone)]
240pub struct DecompressCMintWithCpiContext {
241    /// Mint seed pubkey (used to derive Mint PDA)
242    pub mint_seed_pubkey: Pubkey,
243    /// Fee payer
244    pub payer: Pubkey,
245    /// Mint authority (must sign)
246    pub authority: Pubkey,
247    /// State tree for the compressed mint
248    pub state_tree: Pubkey,
249    /// Input queue for reading compressed mint
250    pub input_queue: Pubkey,
251    /// Output queue for updated compressed mint
252    pub output_queue: Pubkey,
253    /// Compressed mint with context (from indexer)
254    pub compressed_mint_with_context: MintWithContext,
255    /// Validity proof for the compressed mint
256    pub proof: ValidityProof,
257    /// Rent payment in epochs (must be >= 2)
258    pub rent_payment: u8,
259    /// Lamports for future write operations
260    pub write_top_up: u32,
261    /// CPI context account
262    pub cpi_context_pubkey: Pubkey,
263    /// CPI context flags
264    pub cpi_context: CpiContext,
265    /// Compressible config account (ctoken's config)
266    pub compressible_config: Pubkey,
267    /// Rent sponsor account (ctoken's rent sponsor)
268    pub rent_sponsor: Pubkey,
269}
270
271impl DecompressCMintWithCpiContext {
272    pub fn instruction(self) -> Result<Instruction, ProgramError> {
273        // Derive Mint PDA
274        let (mint_pda, _cmint_bump) = crate::instruction::find_mint_address(&self.mint_seed_pubkey);
275
276        // Build DecompressMintAction
277        let action = DecompressMintAction {
278            rent_payment: self.rent_payment,
279            write_top_up: self.write_top_up,
280        };
281
282        // Build instruction data with CPI context
283        let instruction_data = MintActionCompressedInstructionData::new(
284            self.compressed_mint_with_context,
285            self.proof.0,
286        )
287        .with_decompress_mint(action)
288        .with_cpi_context(self.cpi_context.clone());
289
290        // Build account metas with compressible Mint and CPI context
291        // Use provided config/rent_sponsor instead of hardcoded defaults
292        let mut meta_config = MintActionMetaConfig::new(
293            self.payer,
294            self.authority,
295            self.state_tree,
296            self.input_queue,
297            self.output_queue,
298        )
299        .with_compressible_mint(mint_pda, self.compressible_config, self.rent_sponsor);
300
301        meta_config.cpi_context = Some(self.cpi_context_pubkey);
302
303        let account_metas = meta_config.to_account_metas();
304
305        let data = instruction_data
306            .data()
307            .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
308
309        Ok(Instruction {
310            program_id: Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID),
311            accounts: account_metas,
312            data,
313        })
314    }
315}
316
317/// CPI struct for decompressing a mint with CPI context.
318pub struct DecompressCMintCpiWithContext<'info> {
319    /// Mint seed account (used to derive Mint PDA, does not sign)
320    pub mint_seed: AccountInfo<'info>,
321    /// Mint authority (must sign)
322    pub authority: AccountInfo<'info>,
323    /// Fee payer
324    pub payer: AccountInfo<'info>,
325    /// Mint PDA account (writable)
326    pub mint: AccountInfo<'info>,
327    /// CompressibleConfig account
328    pub compressible_config: AccountInfo<'info>,
329    /// Rent sponsor PDA account
330    pub rent_sponsor: AccountInfo<'info>,
331    /// State tree for the compressed mint
332    pub state_tree: AccountInfo<'info>,
333    /// Input queue for reading compressed mint
334    pub input_queue: AccountInfo<'info>,
335    /// Output queue for updated compressed mint
336    pub output_queue: AccountInfo<'info>,
337    /// CPI context account
338    pub cpi_context_account: AccountInfo<'info>,
339    /// System accounts for Light Protocol
340    pub system_accounts: SystemAccountInfos<'info>,
341    /// Light token program's CPI authority (GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy)
342    /// This is separate from system_accounts.cpi_authority_pda which is the calling program's authority
343    pub light_token_cpi_authority: AccountInfo<'info>,
344    /// Compressed mint with context (from indexer)
345    pub compressed_mint_with_context: MintWithContext,
346    /// Validity proof for the compressed mint
347    pub proof: ValidityProof,
348    /// Rent payment in epochs (must be >= 2)
349    pub rent_payment: u8,
350    /// Lamports for future write operations
351    pub write_top_up: u32,
352    /// CPI context flags
353    pub cpi_context: CpiContext,
354}
355
356impl<'info> DecompressCMintCpiWithContext<'info> {
357    pub fn instruction(&self) -> Result<Instruction, ProgramError> {
358        DecompressCMintWithCpiContext {
359            mint_seed_pubkey: *self.mint_seed.key,
360            payer: *self.payer.key,
361            authority: *self.authority.key,
362            state_tree: *self.state_tree.key,
363            input_queue: *self.input_queue.key,
364            output_queue: *self.output_queue.key,
365            compressed_mint_with_context: self.compressed_mint_with_context.clone(),
366            proof: self.proof,
367            rent_payment: self.rent_payment,
368            write_top_up: self.write_top_up,
369            cpi_context_pubkey: *self.cpi_context_account.key,
370            cpi_context: self.cpi_context.clone(),
371            compressible_config: *self.compressible_config.key,
372            rent_sponsor: *self.rent_sponsor.key,
373        }
374        .instruction()
375    }
376
377    pub fn invoke(self) -> Result<(), ProgramError> {
378        let instruction = self.instruction()?;
379        let account_infos = self.build_account_infos();
380        invoke(&instruction, &account_infos)
381    }
382
383    pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
384        let instruction = self.instruction()?;
385        let account_infos = self.build_account_infos();
386        invoke_signed(&instruction, &account_infos, signer_seeds)
387    }
388
389    fn build_account_infos(&self) -> Vec<AccountInfo<'info>> {
390        vec![
391            self.system_accounts.light_system_program.clone(),
392            self.mint_seed.clone(),
393            self.authority.clone(),
394            self.compressible_config.clone(),
395            self.mint.clone(),
396            self.rent_sponsor.clone(),
397            self.payer.clone(),
398            // Use ctoken's CPI authority for the CPI, not the calling program's authority
399            self.light_token_cpi_authority.clone(),
400            self.system_accounts.registered_program_pda.clone(),
401            self.system_accounts.account_compression_authority.clone(),
402            self.system_accounts.account_compression_program.clone(),
403            self.system_accounts.system_program.clone(),
404            self.cpi_context_account.clone(),
405            self.output_queue.clone(),
406            self.state_tree.clone(),
407            self.input_queue.clone(),
408        ]
409    }
410}
411
412/// Helper to create CPI context for first write (first_set_context = true)
413pub fn create_decompress_mint_cpi_context_first(
414    address_tree_pubkey: [u8; 32],
415    tree_index: u8,
416    queue_index: u8,
417) -> CpiContext {
418    CpiContext {
419        first_set_context: true,
420        set_context: false,
421        in_tree_index: tree_index,
422        in_queue_index: queue_index,
423        out_queue_index: queue_index,
424        token_out_queue_index: 0,
425        assigned_account_index: 0,
426        read_only_address_trees: [0; 4],
427        address_tree_pubkey,
428    }
429}
430
431/// Helper to create CPI context for subsequent writes (set_context = true)
432pub fn create_decompress_mint_cpi_context_set(
433    address_tree_pubkey: [u8; 32],
434    tree_index: u8,
435    queue_index: u8,
436) -> CpiContext {
437    CpiContext {
438        first_set_context: false,
439        set_context: true,
440        in_tree_index: tree_index,
441        in_queue_index: queue_index,
442        out_queue_index: queue_index,
443        token_out_queue_index: 0,
444        assigned_account_index: 0,
445        read_only_address_trees: [0; 4],
446        address_tree_pubkey,
447    }
448}
449
450/// Helper to create CPI context for execution (both false - consumes context)
451pub fn create_decompress_mint_cpi_context_execute(
452    address_tree_pubkey: [u8; 32],
453    tree_index: u8,
454    queue_index: u8,
455) -> CpiContext {
456    CpiContext {
457        first_set_context: false,
458        set_context: false,
459        in_tree_index: tree_index,
460        in_queue_index: queue_index,
461        out_queue_index: queue_index,
462        token_out_queue_index: 0,
463        assigned_account_index: 0,
464        read_only_address_trees: [0; 4],
465        address_tree_pubkey,
466    }
467}