light_token/instruction/
create_mint.rs

1use light_compressed_account::instruction_data::{
2    compressed_proof::CompressedProof, traits::LightInstructionData,
3};
4use light_token_interface::{
5    instructions::{
6        extensions::ExtensionInstructionData,
7        mint_action::{CpiContext, DecompressMintAction, MintInstructionData},
8    },
9    COMPRESSED_MINT_SEED,
10};
11use solana_account_info::AccountInfo;
12use solana_cpi::{invoke, invoke_signed};
13use solana_instruction::Instruction;
14use solana_program_error::ProgramError;
15use solana_pubkey::Pubkey;
16
17use super::{config_pda, rent_sponsor_pda};
18use crate::{compressed_token::mint_action::MintActionMetaConfig, instruction::SystemAccountInfos};
19/// Parameters for creating a mint.
20///
21/// Creates both a compressed mint AND a decompressed Mint Solana account
22/// in a single instruction.
23#[derive(Debug, Clone)]
24pub struct CreateMintParams {
25    pub decimals: u8,
26    pub address_merkle_tree_root_index: u16,
27    pub mint_authority: Pubkey,
28    pub proof: CompressedProof,
29    pub compression_address: [u8; 32],
30    pub mint: Pubkey,
31    pub bump: u8,
32    pub freeze_authority: Option<Pubkey>,
33    pub extensions: Option<Vec<ExtensionInstructionData>>,
34    /// Rent payment in epochs for the Mint account (must be 0 or >= 2).
35    /// Default: 16 (~24 hours)
36    pub rent_payment: u8,
37    /// Lamports allocated for future write operations.
38    /// Default: 766 (~3 hours per write)
39    pub write_top_up: u32,
40}
41
42/// Create a mint instruction that creates both a compressed mint AND a Mint Solana account.
43///
44/// # Example
45/// ```rust,no_run
46/// # use solana_pubkey::Pubkey;
47/// use light_token::instruction::{
48///     CreateMint, CreateMintParams, derive_mint_compressed_address, find_mint_address,
49/// };
50/// # use light_token::CompressedProof;
51/// # let mint_seed_pubkey = Pubkey::new_unique();
52/// # let payer = Pubkey::new_unique();
53/// # let address_tree = Pubkey::new_unique();
54/// # let output_queue = Pubkey::new_unique();
55/// # let mint_authority = Pubkey::new_unique();
56/// # let address_merkle_tree_root_index: u16 = 0;
57/// # let proof: CompressedProof = todo!();
58///
59/// // Derive addresses
60/// let compression_address = derive_mint_compressed_address(&mint_seed_pubkey, &address_tree);
61/// let (mint, bump) = find_mint_address(&mint_seed_pubkey);
62///
63/// let params = CreateMintParams {
64///     decimals: 9,
65///     address_merkle_tree_root_index, // from rpc.get_validity_proof
66///     mint_authority,
67///     proof, // from rpc.get_validity_proof
68///     compression_address,
69///     mint,
70///     bump,
71///     freeze_authority: None,
72///     extensions: None,
73///     rent_payment: 16,  // ~24 hours rent
74///     write_top_up: 766, // ~3 hours per write
75/// };
76/// let instruction = CreateMint::new(
77///     params,
78///     mint_seed_pubkey,
79///     payer,
80///     address_tree,
81///     output_queue,
82/// ).instruction()?;
83/// # Ok::<(), solana_program_error::ProgramError>(())
84/// ```
85#[derive(Debug, Clone)]
86pub struct CreateMint {
87    /// Used as seed for the mint address.
88    /// The mint seed account must be a signer.
89    pub mint_seed_pubkey: Pubkey,
90    pub payer: Pubkey,
91    pub address_tree_pubkey: Pubkey,
92    pub output_queue: Pubkey,
93    pub cpi_context: Option<CpiContext>,
94    pub cpi_context_pubkey: Option<Pubkey>,
95    pub params: CreateMintParams,
96}
97
98impl CreateMint {
99    pub fn new(
100        params: CreateMintParams,
101        mint_seed_pubkey: Pubkey,
102        payer: Pubkey,
103        address_tree_pubkey: Pubkey,
104        output_queue: Pubkey,
105    ) -> Self {
106        Self {
107            mint_seed_pubkey,
108            payer,
109            address_tree_pubkey,
110            output_queue,
111            cpi_context: None,
112            cpi_context_pubkey: None,
113            params,
114        }
115    }
116
117    pub fn with_cpi_context(mut self, cpi_context: CpiContext, cpi_context_pubkey: Pubkey) -> Self {
118        self.cpi_context = Some(cpi_context);
119        self.cpi_context_pubkey = Some(cpi_context_pubkey);
120        self
121    }
122
123    pub fn instruction(self) -> Result<Instruction, ProgramError> {
124        let compressed_mint_instruction_data = MintInstructionData {
125            supply: 0,
126            decimals: self.params.decimals,
127            metadata: light_token_interface::state::MintMetadata {
128                version: 3,
129                mint: self.params.mint.to_bytes().into(),
130                mint_decompressed: false,
131                mint_signer: self.mint_seed_pubkey.to_bytes(),
132                bump: self.params.bump,
133            },
134            mint_authority: Some(self.params.mint_authority.to_bytes().into()),
135            freeze_authority: self
136                .params
137                .freeze_authority
138                .map(|auth| auth.to_bytes().into()),
139            extensions: self.params.extensions,
140        };
141
142        let mut instruction_data =
143            light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint(
144                self.params.address_merkle_tree_root_index,
145                self.params.proof,
146                compressed_mint_instruction_data,
147            );
148
149        // Always add decompress action to create Mint Solana account
150        instruction_data = instruction_data.with_decompress_mint(DecompressMintAction {
151            rent_payment: self.params.rent_payment,
152            write_top_up: self.params.write_top_up,
153        });
154
155        if let Some(ctx) = self.cpi_context {
156            instruction_data = instruction_data.with_cpi_context(ctx);
157        }
158
159        let mut meta_config = MintActionMetaConfig::new_create_mint(
160            self.payer,
161            self.params.mint_authority,
162            self.mint_seed_pubkey,
163            self.address_tree_pubkey,
164            self.output_queue,
165        )
166        // Always include compressible accounts for Mint creation
167        .with_compressible_mint(self.params.mint, config_pda(), rent_sponsor_pda());
168
169        if let Some(cpi_context_pubkey) = self.cpi_context_pubkey {
170            meta_config.cpi_context = Some(cpi_context_pubkey);
171        }
172
173        let account_metas = meta_config.to_account_metas();
174
175        let data = instruction_data
176            .data()
177            .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
178
179        Ok(Instruction {
180            program_id: Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID),
181            accounts: account_metas,
182            data,
183        })
184    }
185}
186
187// ============================================================================
188// AccountInfos Struct: CreateMintCpi (for CPI usage)
189// ============================================================================
190
191/// # Create a mint via CPI:
192/// ```rust,no_run
193/// # use light_token::instruction::{CreateMintCpi, CreateMintParams, SystemAccountInfos};
194/// # use solana_account_info::AccountInfo;
195/// # let mint_seed: AccountInfo = todo!();
196/// # let authority: AccountInfo = todo!();
197/// # let payer: AccountInfo = todo!();
198/// # let address_tree: AccountInfo = todo!();
199/// # let output_queue: AccountInfo = todo!();
200/// # let compressible_config: AccountInfo = todo!();
201/// # let mint: AccountInfo = todo!();
202/// # let rent_sponsor: AccountInfo = todo!();
203/// # let system_accounts: SystemAccountInfos = todo!();
204/// # let params: CreateMintParams = todo!();
205/// CreateMintCpi {
206///     mint_seed,
207///     authority,
208///     payer,
209///     address_tree,
210///     output_queue,
211///     compressible_config,
212///     mint,
213///     rent_sponsor,
214///     system_accounts,
215///     cpi_context: None,
216///     cpi_context_account: None,
217///     params,
218/// }
219/// .invoke()?;
220/// # Ok::<(), solana_program_error::ProgramError>(())
221/// ```
222pub struct CreateMintCpi<'info> {
223    pub mint_seed: AccountInfo<'info>,
224    /// The authority for the mint (will be stored as mint_authority).
225    pub authority: AccountInfo<'info>,
226    /// The fee payer for the transaction.
227    pub payer: AccountInfo<'info>,
228    pub address_tree: AccountInfo<'info>,
229    pub output_queue: AccountInfo<'info>,
230    /// CompressibleConfig account (required for Mint creation)
231    pub compressible_config: AccountInfo<'info>,
232    /// Mint PDA account (writable, will be initialized)
233    pub mint: AccountInfo<'info>,
234    /// Rent sponsor PDA (required for Mint creation)
235    pub rent_sponsor: AccountInfo<'info>,
236    pub system_accounts: SystemAccountInfos<'info>,
237    pub cpi_context: Option<CpiContext>,
238    pub cpi_context_account: Option<AccountInfo<'info>>,
239    pub params: CreateMintParams,
240}
241
242impl<'info> CreateMintCpi<'info> {
243    #[allow(clippy::too_many_arguments)]
244    pub fn new(
245        mint_seed: AccountInfo<'info>,
246        authority: AccountInfo<'info>,
247        payer: AccountInfo<'info>,
248        address_tree: AccountInfo<'info>,
249        output_queue: AccountInfo<'info>,
250        compressible_config: AccountInfo<'info>,
251        mint: AccountInfo<'info>,
252        rent_sponsor: AccountInfo<'info>,
253        system_accounts: SystemAccountInfos<'info>,
254        params: CreateMintParams,
255    ) -> Self {
256        Self {
257            mint_seed,
258            authority,
259            payer,
260            address_tree,
261            output_queue,
262            compressible_config,
263            mint,
264            rent_sponsor,
265            system_accounts,
266            cpi_context: None,
267            cpi_context_account: None,
268            params,
269        }
270    }
271
272    pub fn instruction(&self) -> Result<Instruction, ProgramError> {
273        CreateMint::try_from(self)?.instruction()
274    }
275
276    pub fn invoke(self) -> Result<(), ProgramError> {
277        let instruction = self.instruction()?;
278
279        // Account order must match MintActionMetaConfig::to_account_metas()
280        let mut account_infos = vec![
281            self.system_accounts.light_system_program,
282            self.mint_seed,
283            self.authority,
284            self.compressible_config,
285            self.mint,
286            self.rent_sponsor,
287            self.payer,
288            self.system_accounts.cpi_authority_pda,
289            self.system_accounts.registered_program_pda,
290            self.system_accounts.account_compression_authority,
291            self.system_accounts.account_compression_program,
292            self.system_accounts.system_program,
293        ];
294
295        if let Some(cpi_context_account) = self.cpi_context_account {
296            account_infos.push(cpi_context_account);
297        }
298
299        account_infos.push(self.output_queue);
300        account_infos.push(self.address_tree);
301
302        invoke(&instruction, &account_infos)
303    }
304
305    pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
306        let instruction = self.instruction()?;
307
308        // Account order must match MintActionMetaConfig::to_account_metas()
309        let mut account_infos = vec![
310            self.system_accounts.light_system_program,
311            self.mint_seed,
312            self.authority,
313            self.compressible_config,
314            self.mint,
315            self.rent_sponsor,
316            self.payer,
317            self.system_accounts.cpi_authority_pda,
318            self.system_accounts.registered_program_pda,
319            self.system_accounts.account_compression_authority,
320            self.system_accounts.account_compression_program,
321            self.system_accounts.system_program,
322        ];
323
324        if let Some(cpi_context_account) = self.cpi_context_account {
325            account_infos.push(cpi_context_account);
326        }
327
328        account_infos.push(self.output_queue);
329        account_infos.push(self.address_tree);
330
331        invoke_signed(&instruction, &account_infos, signer_seeds)
332    }
333}
334
335impl<'info> TryFrom<&CreateMintCpi<'info>> for CreateMint {
336    type Error = ProgramError;
337
338    fn try_from(account_infos: &CreateMintCpi<'info>) -> Result<Self, Self::Error> {
339        if account_infos.params.mint_authority != *account_infos.authority.key {
340            solana_msg::msg!(
341                "CreateMintCpi: params.mint_authority ({}) does not match authority account ({})",
342                account_infos.params.mint_authority,
343                account_infos.authority.key
344            );
345            return Err(ProgramError::InvalidAccountData);
346        }
347        Ok(Self {
348            mint_seed_pubkey: *account_infos.mint_seed.key,
349            payer: *account_infos.payer.key,
350            address_tree_pubkey: *account_infos.address_tree.key,
351            output_queue: *account_infos.output_queue.key,
352            cpi_context: account_infos.cpi_context.clone(),
353            cpi_context_pubkey: account_infos
354                .cpi_context_account
355                .as_ref()
356                .map(|acc| *acc.key),
357            params: account_infos.params.clone(),
358        })
359    }
360}
361
362// ============================================================================
363// Helper Functions
364// ============================================================================
365
366/// Derives the compressed mint address from the mint seed and address tree
367pub fn derive_mint_compressed_address(
368    mint_seed: &Pubkey,
369    address_tree_pubkey: &Pubkey,
370) -> [u8; 32] {
371    light_compressed_account::address::derive_address(
372        &find_mint_address(mint_seed).0.to_bytes(),
373        &address_tree_pubkey.to_bytes(),
374        &light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
375    )
376}
377
378/// Derives the compressed mint address from an SPL mint address
379pub fn derive_mint_from_spl_mint(mint: &Pubkey, address_tree_pubkey: &Pubkey) -> [u8; 32] {
380    light_compressed_account::address::derive_address(
381        &mint.to_bytes(),
382        &address_tree_pubkey.to_bytes(),
383        &light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
384    )
385}
386
387/// Finds the compressed mint address from a mint seed.
388pub fn find_mint_address(mint_seed: &Pubkey) -> (Pubkey, u8) {
389    Pubkey::find_program_address(
390        &[COMPRESSED_MINT_SEED, mint_seed.as_ref()],
391        &Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID),
392    )
393}