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/// }.instruction()?;
25/// # Ok::<(), solana_program_error::ProgramError>(())
26/// ```
27pub struct TransferChecked {
28    pub source: Pubkey,
29    pub mint: Pubkey,
30    pub destination: Pubkey,
31    pub amount: u64,
32    pub decimals: u8,
33    pub authority: Pubkey,
34    /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (0 = no limit)
35    /// When set to a non-zero value, includes max_top_up in instruction data
36    pub max_top_up: Option<u16>,
37}
38
39/// # Transfer ctoken checked via CPI:
40/// ```rust,no_run
41/// # use light_token::instruction::TransferCheckedCpi;
42/// # use solana_account_info::AccountInfo;
43/// # let source: AccountInfo = todo!();
44/// # let mint: AccountInfo = todo!();
45/// # let destination: AccountInfo = todo!();
46/// # let authority: AccountInfo = todo!();
47/// TransferCheckedCpi {
48///     source,
49///     mint,
50///     destination,
51///     amount: 100,
52///     decimals: 9,
53///     authority,
54///     max_top_up: None,
55/// }
56/// .invoke()?;
57/// # Ok::<(), solana_program_error::ProgramError>(())
58/// ```
59pub struct TransferCheckedCpi<'info> {
60    pub source: AccountInfo<'info>,
61    pub mint: AccountInfo<'info>,
62    pub destination: AccountInfo<'info>,
63    pub amount: u64,
64    pub decimals: u8,
65    pub authority: AccountInfo<'info>,
66    /// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (0 = no limit)
67    pub max_top_up: Option<u16>,
68}
69
70impl<'info> TransferCheckedCpi<'info> {
71    pub fn instruction(&self) -> Result<Instruction, ProgramError> {
72        TransferChecked::from(self).instruction()
73    }
74
75    pub fn invoke(self) -> Result<(), ProgramError> {
76        let instruction = TransferChecked::from(&self).instruction()?;
77        let account_infos = [self.source, self.mint, self.destination, self.authority];
78        invoke(&instruction, &account_infos)
79    }
80
81    pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
82        let instruction = TransferChecked::from(&self).instruction()?;
83        let account_infos = [self.source, self.mint, self.destination, self.authority];
84        invoke_signed(&instruction, &account_infos, signer_seeds)
85    }
86}
87
88impl<'info> From<&TransferCheckedCpi<'info>> for TransferChecked {
89    fn from(account_infos: &TransferCheckedCpi<'info>) -> Self {
90        Self {
91            source: *account_infos.source.key,
92            mint: *account_infos.mint.key,
93            destination: *account_infos.destination.key,
94            amount: account_infos.amount,
95            decimals: account_infos.decimals,
96            authority: *account_infos.authority.key,
97            max_top_up: account_infos.max_top_up,
98        }
99    }
100}
101
102impl TransferChecked {
103    pub fn instruction(self) -> Result<Instruction, ProgramError> {
104        // Authority is writable only when max_top_up is set (for compressible top-up lamport transfer)
105        let authority_meta = if self.max_top_up.is_some() {
106            AccountMeta::new(self.authority, true)
107        } else {
108            AccountMeta::new_readonly(self.authority, true)
109        };
110
111        let mut accounts = vec![
112            AccountMeta::new(self.source, false),
113            AccountMeta::new_readonly(self.mint, false),
114            AccountMeta::new(self.destination, false),
115            authority_meta,
116        ];
117
118        // Include system program for compressible top-up when max_top_up is set
119        if self.max_top_up.is_some() {
120            accounts.push(AccountMeta::new_readonly(
121                solana_pubkey::pubkey!("11111111111111111111111111111111"),
122                false,
123            ));
124        }
125
126        Ok(Instruction {
127            program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID),
128            accounts,
129            data: {
130                // Discriminator (1) + amount (8) + decimals (1) + optional max_top_up (2)
131                let mut data = vec![12u8]; // TransferChecked discriminator (SPL compatible)
132                data.extend_from_slice(&self.amount.to_le_bytes());
133                data.push(self.decimals);
134                // Include max_top_up if set (11-byte format)
135                if let Some(max_top_up) = self.max_top_up {
136                    data.extend_from_slice(&max_top_up.to_le_bytes());
137                }
138                data
139            },
140        })
141    }
142}