light_token/instruction/
burn_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/// # Burn tokens from a ctoken account with decimals validation:
9/// ```rust
10/// # use solana_pubkey::Pubkey;
11/// # use light_token::instruction::BurnChecked;
12/// # let source = Pubkey::new_unique();
13/// # let mint = Pubkey::new_unique();
14/// # let authority = Pubkey::new_unique();
15/// let instruction = BurnChecked {
16///     source,
17///     mint,
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 BurnChecked {
27    /// Light Token account to burn from
28    pub source: Pubkey,
29    /// Mint account (supply tracking)
30    pub mint: Pubkey,
31    /// Amount of tokens to burn
32    pub amount: u64,
33    /// Expected token decimals
34    pub decimals: u8,
35    /// Owner of the Light Token account
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/// # Burn ctoken via CPI with decimals validation:
45/// ```rust,no_run
46/// # use light_token::instruction::BurnCheckedCpi;
47/// # use solana_account_info::AccountInfo;
48/// # let source: AccountInfo = todo!();
49/// # let mint: AccountInfo = todo!();
50/// # let authority: AccountInfo = todo!();
51/// # let system_program: AccountInfo = todo!();
52/// BurnCheckedCpi {
53///     source,
54///     mint,
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 BurnCheckedCpi<'info> {
66    pub source: AccountInfo<'info>,
67    pub mint: 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> BurnCheckedCpi<'info> {
79    pub fn instruction(&self) -> Result<Instruction, ProgramError> {
80        BurnChecked::from(self).instruction()
81    }
82
83    pub fn invoke(self) -> Result<(), ProgramError> {
84        let instruction = BurnChecked::from(&self).instruction()?;
85        if let Some(fee_payer) = self.fee_payer {
86            let account_infos = [
87                self.source,
88                self.mint,
89                self.authority,
90                self.system_program,
91                fee_payer,
92            ];
93            invoke(&instruction, &account_infos)
94        } else {
95            let account_infos = [self.source, self.mint, self.authority, self.system_program];
96            invoke(&instruction, &account_infos)
97        }
98    }
99
100    pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
101        let instruction = BurnChecked::from(&self).instruction()?;
102        if let Some(fee_payer) = self.fee_payer {
103            let account_infos = [
104                self.source,
105                self.mint,
106                self.authority,
107                self.system_program,
108                fee_payer,
109            ];
110            invoke_signed(&instruction, &account_infos, signer_seeds)
111        } else {
112            let account_infos = [self.source, self.mint, self.authority, self.system_program];
113            invoke_signed(&instruction, &account_infos, signer_seeds)
114        }
115    }
116}
117
118impl<'info> From<&BurnCheckedCpi<'info>> for BurnChecked {
119    fn from(cpi: &BurnCheckedCpi<'info>) -> Self {
120        Self {
121            source: *cpi.source.key,
122            mint: *cpi.mint.key,
123            amount: cpi.amount,
124            decimals: cpi.decimals,
125            authority: *cpi.authority.key,
126            max_top_up: cpi.max_top_up,
127            fee_payer: cpi.fee_payer.as_ref().map(|a| *a.key),
128        }
129    }
130}
131
132impl BurnChecked {
133    pub fn instruction(self) -> Result<Instruction, ProgramError> {
134        // Authority is writable only when max_top_up is set AND no fee_payer
135        // (authority pays for top-ups only if no separate fee_payer)
136        let authority_meta = if self.max_top_up.is_some() && self.fee_payer.is_none() {
137            AccountMeta::new(self.authority, true)
138        } else {
139            AccountMeta::new_readonly(self.authority, true)
140        };
141
142        let mut accounts = vec![
143            AccountMeta::new(self.source, false),
144            AccountMeta::new(self.mint, false),
145            authority_meta,
146            // System program required for rent top-up CPIs
147            AccountMeta::new_readonly(Pubkey::default(), false),
148        ];
149
150        // Add fee_payer if provided (must be signer and writable)
151        if let Some(fee_payer) = self.fee_payer {
152            accounts.push(AccountMeta::new(fee_payer, true));
153        }
154
155        Ok(Instruction {
156            program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID),
157            accounts,
158            data: {
159                let mut data = vec![15u8]; // CTokenBurnChecked discriminator
160                data.extend_from_slice(&self.amount.to_le_bytes());
161                data.push(self.decimals);
162                // Include max_top_up if set (11-byte format)
163                if let Some(max_top_up) = self.max_top_up {
164                    data.extend_from_slice(&max_top_up.to_le_bytes());
165                }
166                data
167            },
168        })
169    }
170}