Skip to main content

light_token/instruction/
create_mints.rs

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