Skip to main content

light_token/instruction/
decompress_mint.rs

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