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