light_token/instruction/
mint_to_checked.rs

1use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID;
2use solana_account_info::AccountInfo;
3use solana_cpi::{invoke, invoke_signed};
4use solana_instruction::{AccountMeta, Instruction};
5use solana_program_error::ProgramError;
6use solana_pubkey::Pubkey;
7
8/// # Mint tokens to a ctoken account with decimals validation:
9/// ```rust
10/// # use solana_pubkey::Pubkey;
11/// # use light_token::instruction::MintToChecked;
12/// # let mint = Pubkey::new_unique();
13/// # let destination = Pubkey::new_unique();
14/// # let authority = Pubkey::new_unique();
15/// let instruction = MintToChecked {
16///     mint,
17///     destination,
18///     amount: 100,
19///     decimals: 8,
20///     authority,
21///     max_top_up: None,
22///     fee_payer: None,
23/// }.instruction()?;
24/// # Ok::<(), solana_program_error::ProgramError>(())
25/// ```
26pub struct MintToChecked {
27    /// Mint account (supply tracking)
28    pub mint: Pubkey,
29    /// Destination Light Token account to mint to
30    pub destination: Pubkey,
31    /// Amount of tokens to mint
32    pub amount: u64,
33    /// Expected token decimals
34    pub decimals: u8,
35    /// Mint authority
36    pub authority: Pubkey,
37    /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (0 = no limit)
38    /// When set to a non-zero value, includes max_top_up in instruction data
39    pub max_top_up: Option<u16>,
40    /// Optional fee payer for rent top-ups. If not provided, authority pays.
41    pub fee_payer: Option<Pubkey>,
42}
43
44/// # Mint to ctoken via CPI with decimals validation:
45/// ```rust,no_run
46/// # use light_token::instruction::MintToCheckedCpi;
47/// # use solana_account_info::AccountInfo;
48/// # let mint: AccountInfo = todo!();
49/// # let destination: AccountInfo = todo!();
50/// # let authority: AccountInfo = todo!();
51/// # let system_program: AccountInfo = todo!();
52/// MintToCheckedCpi {
53///     mint,
54///     destination,
55///     amount: 100,
56///     decimals: 8,
57///     authority,
58///     system_program,
59///     max_top_up: None,
60///     fee_payer: None,
61/// }
62/// .invoke()?;
63/// # Ok::<(), solana_program_error::ProgramError>(())
64/// ```
65pub struct MintToCheckedCpi<'info> {
66    pub mint: AccountInfo<'info>,
67    pub destination: AccountInfo<'info>,
68    pub amount: u64,
69    pub decimals: u8,
70    pub authority: AccountInfo<'info>,
71    pub system_program: AccountInfo<'info>,
72    /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (0 = no limit)
73    pub max_top_up: Option<u16>,
74    /// Optional fee payer for rent top-ups. If not provided, authority pays.
75    pub fee_payer: Option<AccountInfo<'info>>,
76}
77
78impl<'info> MintToCheckedCpi<'info> {
79    pub fn instruction(&self) -> Result<Instruction, ProgramError> {
80        MintToChecked::from(self).instruction()
81    }
82
83    pub fn invoke(self) -> Result<(), ProgramError> {
84        let instruction = MintToChecked::from(&self).instruction()?;
85        if let Some(fee_payer) = self.fee_payer {
86            let account_infos = [
87                self.mint,
88                self.destination,
89                self.authority,
90                self.system_program,
91                fee_payer,
92            ];
93            invoke(&instruction, &account_infos)
94        } else {
95            let account_infos = [
96                self.mint,
97                self.destination,
98                self.authority,
99                self.system_program,
100            ];
101            invoke(&instruction, &account_infos)
102        }
103    }
104
105    pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
106        let instruction = MintToChecked::from(&self).instruction()?;
107        if let Some(fee_payer) = self.fee_payer {
108            let account_infos = [
109                self.mint,
110                self.destination,
111                self.authority,
112                self.system_program,
113                fee_payer,
114            ];
115            invoke_signed(&instruction, &account_infos, signer_seeds)
116        } else {
117            let account_infos = [
118                self.mint,
119                self.destination,
120                self.authority,
121                self.system_program,
122            ];
123            invoke_signed(&instruction, &account_infos, signer_seeds)
124        }
125    }
126}
127
128impl<'info> From<&MintToCheckedCpi<'info>> for MintToChecked {
129    fn from(cpi: &MintToCheckedCpi<'info>) -> Self {
130        Self {
131            mint: *cpi.mint.key,
132            destination: *cpi.destination.key,
133            amount: cpi.amount,
134            decimals: cpi.decimals,
135            authority: *cpi.authority.key,
136            max_top_up: cpi.max_top_up,
137            fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key),
138        }
139    }
140}
141
142impl MintToChecked {
143    pub fn instruction(self) -> Result<Instruction, ProgramError> {
144        // Authority is writable only when max_top_up is set AND no fee_payer
145        // (authority pays for top-ups only if no separate fee_payer)
146        let authority_meta = if self.max_top_up.is_some() && self.fee_payer.is_none() {
147            AccountMeta::new(self.authority, true)
148        } else {
149            AccountMeta::new_readonly(self.authority, true)
150        };
151
152        let mut accounts = vec![
153            AccountMeta::new(self.mint, false),
154            AccountMeta::new(self.destination, false),
155            authority_meta,
156            // System program required for rent top-up CPIs
157            AccountMeta::new_readonly(Pubkey::default(), false),
158        ];
159
160        // Add fee_payer if provided (must be signer and writable)
161        if let Some(fee_payer) = self.fee_payer {
162            accounts.push(AccountMeta::new(fee_payer, true));
163        }
164
165        Ok(Instruction {
166            program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID),
167            accounts,
168            data: {
169                let mut data = vec![14u8]; // TokenMintToChecked discriminator
170                data.extend_from_slice(&self.amount.to_le_bytes());
171                data.push(self.decimals);
172                // Include max_top_up if set (11-byte format)
173                if let Some(max_top_up) = self.max_top_up {
174                    data.extend_from_slice(&max_top_up.to_le_bytes());
175                }
176                data
177            },
178        })
179    }
180}