light_token/instruction/
create_mints.rs

1//! Create multiple compressed mints and decompress all to Solana Mint accounts.
2//!
3//! This module provides functionality for batch creating compressed mints with
4//! optimal CPI batching. When creating multiple mints, it uses the CPI context
5//! pattern to minimize transaction overhead.
6//!
7//! # Flow
8//!
9//! - N=1: Single CPI (create + decompress)
10//! - N>1: 2N-1 CPIs (N-1 writes + 1 execute with decompress + N-1 decompress)
11
12use light_batched_merkle_tree::queue::BatchedQueueAccount;
13use light_compressed_account::instruction_data::traits::LightInstructionData;
14use light_token_interface::{
15    instructions::{
16        extensions::{ExtensionInstructionData, TokenMetadataInstructionData},
17        mint_action::{
18            Action, CpiContext, CreateMint, DecompressMintAction,
19            MintActionCompressedInstructionData, MintInstructionData,
20        },
21    },
22    state::MintMetadata,
23    LIGHT_TOKEN_PROGRAM_ID,
24};
25use solana_account_info::AccountInfo;
26use solana_instruction::Instruction;
27use solana_program_error::ProgramError;
28use solana_pubkey::Pubkey;
29
30use super::SystemAccountInfos;
31use crate::compressed_token::mint_action::{
32    get_mint_action_instruction_account_metas_cpi_write, MintActionMetaConfig,
33    MintActionMetaConfigCpiWrite,
34};
35
36/// Default rent payment epochs (~24 hours)
37pub const DEFAULT_RENT_PAYMENT: u8 = 16;
38/// Default lamports for write operations (~3 hours per write)
39pub const DEFAULT_WRITE_TOP_UP: u32 = 766;
40
41/// Parameters for a single mint within a batch creation.
42///
43/// Does not include proof since proof is shared across all mints in the batch.
44#[derive(Debug, Clone)]
45pub struct SingleMintParams<'a> {
46    pub decimals: u8,
47    pub address_merkle_tree_root_index: u16,
48    pub mint_authority: Pubkey,
49    pub compression_address: [u8; 32],
50    pub mint: Pubkey,
51    pub bump: u8,
52    pub freeze_authority: Option<Pubkey>,
53    /// Mint seed pubkey (signer) for this mint
54    pub mint_seed_pubkey: Pubkey,
55    /// Optional authority seeds for PDA signing
56    pub authority_seeds: Option<&'a [&'a [u8]]>,
57    /// Optional mint signer seeds for PDA signing
58    pub mint_signer_seeds: Option<&'a [&'a [u8]]>,
59    /// Optional token metadata for the mint (reference to avoid stack overflow)
60    pub token_metadata: Option<&'a TokenMetadataInstructionData>,
61}
62
63/// Parameters for creating one or more compressed mints with decompression.
64///
65/// Creates N compressed mints and decompresses all to Solana Mint accounts.
66/// Uses CPI context pattern when N > 1 for efficiency.
67#[derive(Debug, Clone)]
68pub struct CreateMintsParams<'a> {
69    /// Parameters for each mint to create
70    pub mints: &'a [SingleMintParams<'a>],
71    /// Single proof covering all new addresses
72    pub proof: light_compressed_account::instruction_data::compressed_proof::CompressedProof,
73    /// Rent payment in epochs for the Mint account (must be 0 or >= 2).
74    /// Default: 16 (~24 hours)
75    pub rent_payment: u8,
76    /// Lamports allocated for future write operations.
77    /// Default: 766 (~3 hours per write)
78    pub write_top_up: u32,
79    /// Offset for assigned_account_index when sharing CPI context with other accounts.
80    /// When creating mints alongside PDAs, this offset should be set to the number of
81    /// PDAs already written to the CPI context.
82    /// Default: 0 (no offset)
83    pub cpi_context_offset: u8,
84    /// Index of the output queue in tree accounts.
85    /// Default: 0
86    pub output_queue_index: u8,
87    /// Index of the address merkle tree in tree accounts.
88    /// Default: 1
89    pub address_tree_index: u8,
90    /// Index of the state merkle tree in tree accounts.
91    /// Required for decompress operations (discriminator validation).
92    /// Default: 2
93    pub state_tree_index: u8,
94}
95
96impl<'a> CreateMintsParams<'a> {
97    #[inline(never)]
98    pub fn new(
99        mints: &'a [SingleMintParams<'a>],
100        proof: light_compressed_account::instruction_data::compressed_proof::CompressedProof,
101    ) -> Self {
102        Self {
103            mints,
104            proof,
105            rent_payment: DEFAULT_RENT_PAYMENT,
106            write_top_up: DEFAULT_WRITE_TOP_UP,
107            cpi_context_offset: 0,
108            output_queue_index: 0,
109            address_tree_index: 1,
110            state_tree_index: 2,
111        }
112    }
113
114    pub fn with_rent_payment(mut self, rent_payment: u8) -> Self {
115        self.rent_payment = rent_payment;
116        self
117    }
118
119    pub fn with_write_top_up(mut self, write_top_up: u32) -> Self {
120        self.write_top_up = write_top_up;
121        self
122    }
123
124    /// Set offset for assigned_account_index when sharing CPI context.
125    ///
126    /// Use this when creating mints alongside PDAs. The offset should be
127    /// the number of accounts already written to the CPI context.
128    pub fn with_cpi_context_offset(mut self, offset: u8) -> Self {
129        self.cpi_context_offset = offset;
130        self
131    }
132
133    /// Set the output queue index in tree accounts.
134    pub fn with_output_queue_index(mut self, index: u8) -> Self {
135        self.output_queue_index = index;
136        self
137    }
138
139    /// Set the address merkle tree index in tree accounts.
140    pub fn with_address_tree_index(mut self, index: u8) -> Self {
141        self.address_tree_index = index;
142        self
143    }
144
145    /// Set the state merkle tree index in tree accounts.
146    /// Required for decompress operations (discriminator validation).
147    pub fn with_state_tree_index(mut self, index: u8) -> Self {
148        self.state_tree_index = index;
149        self
150    }
151}
152
153/// CPI struct for on-chain programs to create multiple mints.
154///
155/// Uses named account fields for clarity and safety - no manual index calculations.
156///
157/// # Example
158///
159/// ```rust,ignore
160/// use light_token::instruction::{CreateMintsCpi, CreateMintsParams, SingleMintParams, SystemAccountInfos};
161///
162/// let params = CreateMintsParams::new(vec![mint_params_1, mint_params_2], proof);
163///
164/// CreateMintsCpi {
165///     mint_seeds: vec![mint_signer1.clone(), mint_signer2.clone()],
166///     payer: payer.clone(),
167///     address_tree: address_tree.clone(),
168///     output_queue: output_queue.clone(),
169///     compressible_config: config.clone(),
170///     mints: vec![mint_pda1.clone(), mint_pda2.clone()],
171///     rent_sponsor: rent_sponsor.clone(),
172///     system_accounts: SystemAccountInfos { ... },
173///     cpi_context_account: cpi_context.clone(),
174///     params,
175/// }.invoke()?;
176/// ```
177pub struct CreateMintsCpi<'a, 'info> {
178    /// Mint seed accounts (signers) - one per mint
179    pub mint_seed_accounts: &'a [AccountInfo<'info>],
180    /// Fee payer (also used as authority)
181    pub payer: AccountInfo<'info>,
182    /// Address tree for new mint addresses
183    pub address_tree: AccountInfo<'info>,
184    /// Output queue for compressed accounts
185    pub output_queue: AccountInfo<'info>,
186    /// State merkle tree (required for decompress discriminator validation)
187    pub state_merkle_tree: AccountInfo<'info>,
188    /// CompressibleConfig account
189    pub compressible_config: AccountInfo<'info>,
190    /// Mint PDA accounts (writable) - one per mint
191    pub mints: &'a [AccountInfo<'info>],
192    /// Rent sponsor PDA
193    pub rent_sponsor: AccountInfo<'info>,
194    /// Standard Light Protocol system accounts
195    pub system_accounts: SystemAccountInfos<'info>,
196    /// CPI context account
197    pub cpi_context_account: AccountInfo<'info>,
198    /// Parameters
199    pub params: CreateMintsParams<'a>,
200}
201
202impl<'a, 'info> CreateMintsCpi<'a, 'info> {
203    /// Validate that the struct is properly constructed.
204    #[inline(never)]
205    pub fn validate(&self) -> Result<(), ProgramError> {
206        let n = self.params.mints.len();
207        if n == 0 {
208            return Err(ProgramError::InvalidArgument);
209        }
210        if self.mint_seed_accounts.len() != n {
211            return Err(ProgramError::InvalidArgument);
212        }
213        if self.mints.len() != n {
214            return Err(ProgramError::InvalidArgument);
215        }
216        Ok(())
217    }
218
219    /// Execute all CPIs to create and decompress all mints.
220    ///
221    /// Signer seeds are extracted from `SingleMintParams::mint_signer_seeds` and
222    /// `SingleMintParams::authority_seeds` for each CPI call (0, 1, or 2 seeds per call).
223    #[inline(never)]
224    pub fn invoke(self) -> Result<(), ProgramError> {
225        self.validate()?;
226        let n = self.params.mints.len();
227
228        // Use single mint path only when:
229        // - N=1 AND
230        // - No CPI context offset (no PDAs were written to CPI context first)
231        if n == 1 && self.params.cpi_context_offset == 0 {
232            self.invoke_single_mint()
233        } else {
234            self.invoke_multiple_mints()
235        }
236    }
237
238    /// Handle the single mint case: create + decompress in one CPI.
239    #[inline(never)]
240    fn invoke_single_mint(self) -> Result<(), ProgramError> {
241        let mint_params = &self.params.mints[0];
242
243        let mint_data = build_mint_instruction_data(mint_params, self.mint_seed_accounts[0].key);
244
245        let decompress_action = DecompressMintAction {
246            rent_payment: self.params.rent_payment,
247            write_top_up: self.params.write_top_up,
248        };
249
250        let instruction_data = MintActionCompressedInstructionData::new_mint(
251            mint_params.address_merkle_tree_root_index,
252            self.params.proof,
253            mint_data,
254        )
255        .with_decompress_mint(decompress_action);
256
257        let mut meta_config = MintActionMetaConfig::new_create_mint(
258            *self.payer.key,
259            *self.payer.key,
260            *self.mint_seed_accounts[0].key,
261            *self.address_tree.key,
262            *self.output_queue.key,
263        )
264        .with_compressible_mint(
265            *self.mints[0].key,
266            *self.compressible_config.key,
267            *self.rent_sponsor.key,
268        );
269        meta_config.input_queue = Some(*self.output_queue.key);
270
271        self.invoke_mint_action(instruction_data, meta_config, 0)
272    }
273
274    /// Handle the multiple mints case: N-1 writes + 1 execute + N-1 decompress.
275    #[inline(never)]
276    fn invoke_multiple_mints(self) -> Result<(), ProgramError> {
277        let n = self.params.mints.len();
278
279        // Get base leaf index before any CPIs modify the queue
280        let base_leaf_index = get_base_leaf_index(&self.output_queue)?;
281
282        let decompress_action = DecompressMintAction {
283            rent_payment: self.params.rent_payment,
284            write_top_up: self.params.write_top_up,
285        };
286
287        // Write mints 0..N-2 to CPI context
288        for i in 0..(n - 1) {
289            self.invoke_cpi_write(i)?;
290        }
291
292        // Execute: create last mint + decompress it
293        self.invoke_execute(n - 1, &decompress_action)?;
294
295        // Decompress remaining mints (0..N-2)
296        for i in 0..(n - 1) {
297            self.invoke_decompress(i, base_leaf_index, &decompress_action)?;
298        }
299
300        Ok(())
301    }
302
303    /// Invoke a CPI write instruction for a single mint.
304    /// Extracts signer seeds from mint params (0, 1, or 2 seeds).
305    #[inline(never)]
306    fn invoke_cpi_write(&self, index: usize) -> Result<(), ProgramError> {
307        let mint_params = &self.params.mints[index];
308        let offset = self.params.cpi_context_offset;
309
310        // When sharing CPI context with PDAs:
311        // - first_set_context: only true for index 0 AND offset 0 (first write to context)
312        // - set_context: true if appending to existing context (index > 0 or offset > 0)
313        // - assigned_account_index: offset + index (to not collide with PDA indices)
314        let cpi_context = CpiContext {
315            set_context: index > 0 || offset > 0,
316            first_set_context: index == 0 && offset == 0,
317            in_tree_index: self.params.address_tree_index,
318            in_queue_index: self.params.output_queue_index,
319            out_queue_index: self.params.output_queue_index,
320            token_out_queue_index: 0,
321            assigned_account_index: offset + index as u8,
322            read_only_address_trees: [0; 4],
323            address_tree_pubkey: self.address_tree.key.to_bytes(),
324        };
325
326        let mint_data =
327            build_mint_instruction_data(mint_params, self.mint_seed_accounts[index].key);
328
329        let instruction_data = MintActionCompressedInstructionData::new_mint_write_to_cpi_context(
330            mint_params.address_merkle_tree_root_index,
331            mint_data,
332            cpi_context,
333        );
334
335        let cpi_write_config = MintActionMetaConfigCpiWrite {
336            fee_payer: *self.payer.key,
337            mint_signer: Some(*self.mint_seed_accounts[index].key),
338            authority: *self.payer.key,
339            cpi_context: *self.cpi_context_account.key,
340        };
341
342        let account_metas = get_mint_action_instruction_account_metas_cpi_write(cpi_write_config);
343        let ix_data = instruction_data
344            .data()
345            .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
346
347        // Account order matches get_mint_action_instruction_account_metas_cpi_write:
348        // [0]: light_system_program
349        // [1]: mint_signer (optional, when present)
350        // [2]: authority
351        // [3]: fee_payer
352        // [4]: cpi_authority_pda
353        // [5]: cpi_context
354        let account_infos = [
355            self.system_accounts.light_system_program.clone(),
356            self.mint_seed_accounts[index].clone(),
357            self.payer.clone(),
358            self.payer.clone(),
359            self.system_accounts.cpi_authority_pda.clone(),
360            self.cpi_context_account.clone(),
361        ];
362        let instruction = Instruction {
363            program_id: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID),
364            accounts: account_metas,
365            data: ix_data,
366        };
367
368        // Build signer seeds - pack present seeds at start of array
369        let mut seeds: [&[&[u8]]; 2] = [&[], &[]];
370        let mut num_signers = 0;
371        if let Some(s) = mint_params.mint_signer_seeds {
372            seeds[num_signers] = s;
373            num_signers += 1;
374        }
375        if let Some(s) = mint_params.authority_seeds {
376            seeds[num_signers] = s;
377            num_signers += 1;
378        }
379        solana_cpi::invoke_signed(&instruction, &account_infos, &seeds[..num_signers])
380    }
381
382    /// Invoke the execute instruction (create last mint + decompress).
383    /// Extracts signer seeds from mint params (0, 1, or 2 seeds).
384    #[inline(never)]
385    fn invoke_execute(
386        &self,
387        last_idx: usize,
388        decompress_action: &DecompressMintAction,
389    ) -> Result<(), ProgramError> {
390        let mint_params = &self.params.mints[last_idx];
391        let offset = self.params.cpi_context_offset;
392
393        let mint_data =
394            build_mint_instruction_data(mint_params, self.mint_seed_accounts[last_idx].key);
395
396        // Create struct directly to reduce stack usage (avoid builder pattern intermediates)
397        let instruction_data = MintActionCompressedInstructionData {
398            leaf_index: 0,
399            prove_by_index: false,
400            root_index: mint_params.address_merkle_tree_root_index,
401            max_top_up: 0,
402            create_mint: Some(CreateMint::default()),
403            actions: vec![Action::DecompressMint(*decompress_action)],
404            proof: Some(self.params.proof),
405            cpi_context: Some(CpiContext {
406                set_context: false,
407                first_set_context: false,
408                in_tree_index: self.params.address_tree_index,
409                in_queue_index: self.params.address_tree_index,
410                out_queue_index: self.params.output_queue_index,
411                token_out_queue_index: 0,
412                assigned_account_index: offset + last_idx as u8,
413                read_only_address_trees: [0; 4],
414                address_tree_pubkey: self.address_tree.key.to_bytes(),
415            }),
416            mint: Some(mint_data),
417        };
418
419        let mut meta_config = MintActionMetaConfig::new_create_mint(
420            *self.payer.key,
421            *self.payer.key,
422            *self.mint_seed_accounts[last_idx].key,
423            *self.address_tree.key,
424            *self.output_queue.key,
425        )
426        .with_compressible_mint(
427            *self.mints[last_idx].key,
428            *self.compressible_config.key,
429            *self.rent_sponsor.key,
430        );
431        meta_config.cpi_context = Some(*self.cpi_context_account.key);
432        meta_config.input_queue = Some(*self.output_queue.key);
433
434        self.invoke_mint_action(instruction_data, meta_config, last_idx)
435    }
436
437    /// Invoke decompress for a single mint.
438    /// Extracts signer seeds from mint params (0, 1, or 2 seeds).
439    #[inline(never)]
440    fn invoke_decompress(
441        &self,
442        index: usize,
443        base_leaf_index: u32,
444        decompress_action: &DecompressMintAction,
445    ) -> Result<(), ProgramError> {
446        let mint_params = &self.params.mints[index];
447
448        let mint_data =
449            build_mint_instruction_data(mint_params, self.mint_seed_accounts[index].key);
450
451        let instruction_data = MintActionCompressedInstructionData {
452            leaf_index: base_leaf_index + index as u32,
453            prove_by_index: true,
454            root_index: 0,
455            max_top_up: 0,
456            create_mint: None,
457            actions: vec![Action::DecompressMint(*decompress_action)],
458            proof: None,
459            cpi_context: None,
460            mint: Some(mint_data),
461        };
462
463        // For prove_by_index, the tree_pubkey must be state_merkle_tree for discriminator validation
464        let meta_config = MintActionMetaConfig::new(
465            *self.payer.key,
466            *self.payer.key,
467            *self.state_merkle_tree.key, // tree_pubkey - state merkle tree for discriminator check
468            *self.output_queue.key,      // input_queue
469            *self.output_queue.key,      // output_queue
470        )
471        .with_compressible_mint(
472            *self.mints[index].key,
473            *self.compressible_config.key,
474            *self.rent_sponsor.key,
475        );
476
477        self.invoke_mint_action(instruction_data, meta_config, index)
478    }
479
480    /// Invoke a mint action instruction.
481    /// Extracts signer seeds from mint params at the given index (0, 1, or 2 seeds).
482    #[inline(never)]
483    fn invoke_mint_action(
484        &self,
485        instruction_data: MintActionCompressedInstructionData,
486        meta_config: MintActionMetaConfig,
487        mint_index: usize,
488    ) -> Result<(), ProgramError> {
489        let account_metas = meta_config.to_account_metas();
490        let ix_data = instruction_data
491            .data()
492            .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
493
494        // Collect all account infos needed for the CPI
495        let mut account_infos = vec![self.payer.clone()];
496
497        // System accounts
498        account_infos.push(self.system_accounts.light_system_program.clone());
499
500        // Add all mint seeds
501        for mint_seed in self.mint_seed_accounts {
502            account_infos.push(mint_seed.clone());
503        }
504
505        // More system accounts
506        account_infos.push(self.system_accounts.cpi_authority_pda.clone());
507        account_infos.push(self.system_accounts.registered_program_pda.clone());
508        account_infos.push(self.system_accounts.account_compression_authority.clone());
509        account_infos.push(self.system_accounts.account_compression_program.clone());
510        account_infos.push(self.system_accounts.system_program.clone());
511
512        // CPI context, queues, trees
513        account_infos.push(self.cpi_context_account.clone());
514        account_infos.push(self.output_queue.clone());
515        account_infos.push(self.state_merkle_tree.clone());
516        account_infos.push(self.address_tree.clone());
517        account_infos.push(self.compressible_config.clone());
518        account_infos.push(self.rent_sponsor.clone());
519
520        // Add all mint PDAs
521        for mint in self.mints {
522            account_infos.push(mint.clone());
523        }
524
525        let instruction = Instruction {
526            program_id: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID),
527            accounts: account_metas,
528            data: ix_data,
529        };
530
531        // Build signer seeds - pack present seeds at start of array
532        let mint_params = &self.params.mints[mint_index];
533        let mut seeds: [&[&[u8]]; 2] = [&[], &[]];
534        let mut num_signers = 0;
535        if let Some(s) = mint_params.mint_signer_seeds {
536            seeds[num_signers] = s;
537            num_signers += 1;
538        }
539        if let Some(s) = mint_params.authority_seeds {
540            seeds[num_signers] = s;
541            num_signers += 1;
542        }
543        solana_cpi::invoke_signed(&instruction, &account_infos, &seeds[..num_signers])
544    }
545}
546
547/// Build MintInstructionData for a single mint.
548#[inline(never)]
549fn build_mint_instruction_data(
550    mint_params: &SingleMintParams<'_>,
551    mint_signer: &Pubkey,
552) -> MintInstructionData {
553    // Convert token_metadata to extensions if present
554    let extensions = mint_params
555        .token_metadata
556        .cloned()
557        .map(|metadata| vec![ExtensionInstructionData::TokenMetadata(metadata)]);
558
559    MintInstructionData {
560        supply: 0,
561        decimals: mint_params.decimals,
562        metadata: MintMetadata {
563            version: 3,
564            mint: mint_params.mint.to_bytes().into(),
565            mint_decompressed: false,
566            mint_signer: mint_signer.to_bytes(),
567            bump: mint_params.bump,
568        },
569        mint_authority: Some(mint_params.mint_authority.to_bytes().into()),
570        freeze_authority: mint_params.freeze_authority.map(|a| a.to_bytes().into()),
571        extensions,
572    }
573}
574
575/// Get base leaf index from output queue account.
576#[inline(never)]
577fn get_base_leaf_index(output_queue: &AccountInfo) -> Result<u32, ProgramError> {
578    let queue = BatchedQueueAccount::output_from_account_info(output_queue)
579        .map_err(|_| ProgramError::InvalidAccountData)?;
580    Ok(queue.batch_metadata.next_index as u32)
581}
582
583/// Create multiple mints and decompress all to Solana accounts.
584///
585/// Convenience function that builds a [`CreateMintsCpi`] from a slice of accounts.
586///
587/// # Arguments
588///
589/// * `payer` - The fee payer account
590/// * `accounts` - The remaining accounts in the expected layout
591/// * `params` - Parameters for creating the mints
592///
593/// # Account Layout
594///
595/// - `[0]`: light_system_program
596/// - `[1..N+1]`: mint_signers (SIGNER)
597/// - `[N+1..N+6]`: system PDAs (cpi_authority, registered_program, compression_authority, compression_program, system_program)
598/// - `[N+6]`: cpi_context_account (writable)
599/// - `[N+7]`: output_queue (writable)
600/// - `[N+8]`: state_merkle_tree (writable)
601/// - `[N+9]`: address_tree (writable)
602/// - `[N+10]`: compressible_config
603/// - `[N+11]`: rent_sponsor (writable)
604/// - `[N+12..2N+12]`: mint_pdas (writable)
605/// - `[2N+12]`: compressed_token_program (for CPI)
606#[inline(never)]
607pub fn create_mints<'a, 'info>(
608    payer: &AccountInfo<'info>,
609    accounts: &'info [AccountInfo<'info>],
610    params: CreateMintsParams<'a>,
611) -> Result<(), ProgramError> {
612    if params.mints.is_empty() {
613        return Err(ProgramError::InvalidArgument);
614    }
615
616    let n = params.mints.len();
617    let mint_signers_start = 1;
618    let cpi_authority_idx = n + 1;
619    let registered_program_idx = n + 2;
620    let compression_authority_idx = n + 3;
621    let compression_program_idx = n + 4;
622    let system_program_idx = n + 5;
623    let cpi_context_idx = n + 6;
624    let output_queue_idx = n + 7;
625    let state_merkle_tree_idx = n + 8;
626    let address_tree_idx = n + 9;
627    let compressible_config_idx = n + 10;
628    let rent_sponsor_idx = n + 11;
629    let mint_pdas_start = n + 12;
630
631    // Build named struct from accounts slice
632    let cpi = CreateMintsCpi {
633        mint_seed_accounts: &accounts[mint_signers_start..mint_signers_start + n],
634        payer: payer.clone(),
635        address_tree: accounts[address_tree_idx].clone(),
636        output_queue: accounts[output_queue_idx].clone(),
637        state_merkle_tree: accounts[state_merkle_tree_idx].clone(),
638        compressible_config: accounts[compressible_config_idx].clone(),
639        mints: &accounts[mint_pdas_start..mint_pdas_start + n],
640        rent_sponsor: accounts[rent_sponsor_idx].clone(),
641        system_accounts: SystemAccountInfos {
642            light_system_program: accounts[0].clone(),
643            cpi_authority_pda: accounts[cpi_authority_idx].clone(),
644            registered_program_pda: accounts[registered_program_idx].clone(),
645            account_compression_authority: accounts[compression_authority_idx].clone(),
646            account_compression_program: accounts[compression_program_idx].clone(),
647            system_program: accounts[system_program_idx].clone(),
648        },
649        cpi_context_account: accounts[cpi_context_idx].clone(),
650        params,
651    };
652    cpi.invoke()
653}
654
655// // ============================================================================
656// // Client-side instruction builder
657// // ============================================================================
658
659// /// Client-side instruction builder for creating multiple mints.
660// ///
661// /// This struct is used to build instructions for client-side transaction construction.
662// /// For CPI usage within Solana programs, use [`CreateMintsCpi`] instead.
663// ///
664// /// # Example
665// ///
666// /// ```rust,ignore
667// /// use light_token::instruction::{CreateMints, CreateMintsParams, SingleMintParams};
668// ///
669// /// let params = CreateMintsParams::new(vec![mint1_params, mint2_params], proof);
670// ///
671// /// let instructions = CreateMints::new(
672// ///     params,
673// ///     mint_seed_pubkeys,
674// ///     payer,
675// ///     address_tree_pubkey,
676// ///     output_queue,
677// ///     state_merkle_tree,
678// ///     cpi_context_pubkey,
679// /// ).instructions()?;
680// /// ```
681// #[derive(Debug, Clone)]
682// pub struct CreateMints<'a> {
683//     pub payer: Pubkey,
684//     pub address_tree_pubkey: Pubkey,
685//     pub output_queue: Pubkey,
686//     pub state_merkle_tree: Pubkey,
687//     pub cpi_context_pubkey: Pubkey,
688//     pub params: CreateMintsParams<'a>,
689// }
690
691// impl<'a> CreateMints<'a> {
692//     #[allow(clippy::too_many_arguments)]
693//     pub fn new(
694//         params: CreateMintsParams<'a>,
695//         payer: Pubkey,
696//         address_tree_pubkey: Pubkey,
697//         output_queue: Pubkey,
698//         state_merkle_tree: Pubkey,
699//         cpi_context_pubkey: Pubkey,
700//     ) -> Self {
701//         Self {
702//             payer,
703//             address_tree_pubkey,
704//             output_queue,
705//             state_merkle_tree,
706//             cpi_context_pubkey,
707//             params,
708//         }
709//     }
710
711//     /// Build account metas for the instruction.
712//     pub fn build_account_metas(&self) -> Vec<AccountMeta> {
713//         let system_accounts = SystemAccounts::default();
714
715//         let mut accounts = vec![AccountMeta::new_readonly(
716//             system_accounts.light_system_program,
717//             false,
718//         )];
719
720//         // Add mint signers (from each SingleMintParams)
721//         for mint_params in self.params.mints {
722//             accounts.push(AccountMeta::new_readonly(
723//                 mint_params.mint_seed_pubkey,
724//                 true,
725//             ));
726//         }
727
728//         // Add system PDAs
729//         accounts.extend(vec![
730//             AccountMeta::new_readonly(system_accounts.cpi_authority_pda, false),
731//             AccountMeta::new_readonly(system_accounts.registered_program_pda, false),
732//             AccountMeta::new_readonly(system_accounts.account_compression_authority, false),
733//             AccountMeta::new_readonly(system_accounts.account_compression_program, false),
734//             AccountMeta::new_readonly(system_accounts.system_program, false),
735//         ]);
736
737//         // CPI context, output queue, address tree
738//         accounts.push(AccountMeta::new(self.cpi_context_pubkey, false));
739//         accounts.push(AccountMeta::new(self.output_queue, false));
740//         accounts.push(AccountMeta::new(self.address_tree_pubkey, false));
741
742//         // Config, rent sponsor
743//         accounts.push(AccountMeta::new_readonly(config_pda(), false));
744//         accounts.push(AccountMeta::new(rent_sponsor_pda(), false));
745
746//         // State merkle tree
747//         accounts.push(AccountMeta::new(self.state_merkle_tree, false));
748
749//         // Add mint PDAs
750//         for mint_params in self.params.mints {
751//             accounts.push(AccountMeta::new(mint_params.mint, false));
752//         }
753
754//         accounts
755//     }
756// }