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