Skip to main content

light_token/instruction/
create_mint.rs

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