light_token/instruction/
transfer_interface.rs

1use solana_account_info::AccountInfo;
2use solana_instruction::{AccountMeta, Instruction};
3use solana_program_error::ProgramError;
4use solana_pubkey::Pubkey;
5
6use super::{
7    transfer::Transfer, transfer_from_spl::TransferFromSpl, transfer_to_spl::TransferToSpl,
8};
9use crate::error::TokenSdkError;
10
11/// Internal enum to classify transfer types based on account owners.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13enum TransferType {
14    /// light -> light
15    LightToLight,
16    /// light -> SPL (decompress)
17    LightToSpl,
18    /// SPL -> light (compress)
19    SplToLight,
20    /// SPL -> SPL (pass-through to SPL token program)
21    SplToSpl,
22}
23
24/// Determine transfer type from account owners.
25///
26/// Returns `Ok(TransferType)` if at least one account is a Light token account.
27/// Returns `Err(UseRegularSplTransfer)` if both accounts are non-Light (SPL) accounts.
28/// Returns `Err(CannotDetermineAccountType)` if an account owner is unrecognized.
29fn determine_transfer_type(
30    source_owner: &Pubkey,
31    destination_owner: &Pubkey,
32) -> Result<TransferType, ProgramError> {
33    use crate::utils::is_light_token_owner;
34
35    let source_is_light = is_light_token_owner(source_owner)
36        .map_err(|_| ProgramError::Custom(TokenSdkError::CannotDetermineAccountType.into()))?;
37    let dest_is_light = is_light_token_owner(destination_owner)
38        .map_err(|_| ProgramError::Custom(TokenSdkError::CannotDetermineAccountType.into()))?;
39
40    match (source_is_light, dest_is_light) {
41        (true, true) => Ok(TransferType::LightToLight),
42        (true, false) => Ok(TransferType::LightToSpl),
43        (false, true) => Ok(TransferType::SplToLight),
44        (false, false) => {
45            // Both are SPL - verify same token program
46            if source_owner == destination_owner {
47                Ok(TransferType::SplToSpl)
48            } else {
49                Err(ProgramError::Custom(
50                    TokenSdkError::SplTokenProgramMismatch.into(),
51                ))
52            }
53        }
54    }
55}
56
57/// Required accounts to interface between light and SPL token accounts (Pubkey-based).
58///
59/// Use this struct when building instructions outside of CPI context.
60#[derive(Debug, Clone, Copy)]
61pub struct SplInterface {
62    pub mint: Pubkey,
63    pub spl_token_program: Pubkey,
64    pub spl_interface_pda: Pubkey,
65    pub spl_interface_pda_bump: u8,
66}
67
68impl<'info> From<&SplInterfaceCpi<'info>> for SplInterface {
69    fn from(spl: &SplInterfaceCpi<'info>) -> Self {
70        Self {
71            mint: *spl.mint.key,
72            spl_token_program: *spl.spl_token_program.key,
73            spl_interface_pda: *spl.spl_interface_pda.key,
74            spl_interface_pda_bump: spl.spl_interface_pda_bump,
75        }
76    }
77}
78
79/// Required accounts to interface between light and SPL token accounts (AccountInfo-based).
80///
81/// Use this struct when building CPIs.
82pub struct SplInterfaceCpi<'info> {
83    pub mint: AccountInfo<'info>,
84    pub spl_token_program: AccountInfo<'info>,
85    pub spl_interface_pda: AccountInfo<'info>,
86    pub spl_interface_pda_bump: u8,
87}
88
89/// # Create a transfer interface instruction that auto-routes based on account types:
90/// ```rust
91/// # use solana_pubkey::Pubkey;
92/// # use light_token::instruction::{TransferInterface, SplInterface, LIGHT_TOKEN_PROGRAM_ID};
93/// # let source = Pubkey::new_unique();
94/// # let destination = Pubkey::new_unique();
95/// # let authority = Pubkey::new_unique();
96/// # let payer = Pubkey::new_unique();
97/// // For light -> light transfer (source_owner and destination_owner are LIGHT_TOKEN_PROGRAM_ID)
98/// let instruction = TransferInterface {
99///     source,
100///     destination,
101///     amount: 100,
102///     decimals: 9,
103///     authority,
104///     payer,
105///     spl_interface: None,
106///     max_top_up: None,
107///     source_owner: LIGHT_TOKEN_PROGRAM_ID,
108///     destination_owner: LIGHT_TOKEN_PROGRAM_ID,
109/// }.instruction()?;
110/// # Ok::<(), solana_program_error::ProgramError>(())
111/// ```
112pub struct TransferInterface {
113    pub source: Pubkey,
114    pub destination: Pubkey,
115    pub amount: u64,
116    pub decimals: u8,
117    pub authority: Pubkey,
118    pub payer: Pubkey,
119    pub spl_interface: Option<SplInterface>,
120    /// Maximum lamports for rent and top-up combined (for light->light transfers)
121    pub max_top_up: Option<u16>,
122    /// Owner of the source account (used to determine transfer type)
123    pub source_owner: Pubkey,
124    /// Owner of the destination account (used to determine transfer type)
125    pub destination_owner: Pubkey,
126}
127
128impl TransferInterface {
129    /// Build instruction based on detected transfer type
130    pub fn instruction(self) -> Result<Instruction, ProgramError> {
131        match determine_transfer_type(&self.source_owner, &self.destination_owner)? {
132            TransferType::LightToLight => Transfer {
133                source: self.source,
134                destination: self.destination,
135                amount: self.amount,
136                authority: self.authority,
137                max_top_up: self.max_top_up,
138            }
139            .instruction(),
140
141            TransferType::LightToSpl => {
142                let spl = self.spl_interface.ok_or_else(|| {
143                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
144                })?;
145                TransferToSpl {
146                    source: self.source,
147                    destination_spl_token_account: self.destination,
148                    amount: self.amount,
149                    authority: self.authority,
150                    mint: spl.mint,
151                    payer: self.payer,
152                    spl_interface_pda: spl.spl_interface_pda,
153                    spl_interface_pda_bump: spl.spl_interface_pda_bump,
154                    decimals: self.decimals,
155                    spl_token_program: spl.spl_token_program,
156                }
157                .instruction()
158            }
159
160            TransferType::SplToLight => {
161                let spl = self.spl_interface.ok_or_else(|| {
162                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
163                })?;
164                TransferFromSpl {
165                    source_spl_token_account: self.source,
166                    destination: self.destination,
167                    amount: self.amount,
168                    authority: self.authority,
169                    mint: spl.mint,
170                    payer: self.payer,
171                    spl_interface_pda: spl.spl_interface_pda,
172                    spl_interface_pda_bump: spl.spl_interface_pda_bump,
173                    decimals: self.decimals,
174                    spl_token_program: spl.spl_token_program,
175                }
176                .instruction()
177            }
178
179            TransferType::SplToSpl => {
180                let spl = self.spl_interface.ok_or_else(|| {
181                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
182                })?;
183
184                // Build SPL transfer_checked instruction manually
185                // Discriminator 12 = TransferChecked
186                let mut data = vec![12u8];
187                data.extend_from_slice(&self.amount.to_le_bytes());
188                data.push(self.decimals);
189
190                Ok(Instruction {
191                    program_id: self.source_owner, // SPL token program
192                    accounts: vec![
193                        AccountMeta::new(self.source, false),
194                        AccountMeta::new_readonly(spl.mint, false),
195                        AccountMeta::new(self.destination, false),
196                        AccountMeta::new_readonly(self.authority, true),
197                    ],
198                    data,
199                })
200            }
201        }
202    }
203}
204
205impl<'info> From<&TransferInterfaceCpi<'info>> for TransferInterface {
206    fn from(cpi: &TransferInterfaceCpi<'info>) -> Self {
207        Self {
208            source: *cpi.source_account.key,
209            destination: *cpi.destination_account.key,
210            amount: cpi.amount,
211            decimals: cpi.decimals,
212            authority: *cpi.authority.key,
213            payer: *cpi.payer.key,
214            spl_interface: cpi.spl_interface.as_ref().map(SplInterface::from),
215            max_top_up: None,
216            source_owner: *cpi.source_account.owner,
217            destination_owner: *cpi.destination_account.owner,
218        }
219    }
220}
221
222/// # Transfer interface via CPI (auto-detects account types):
223/// ```rust,no_run
224/// # use light_token::instruction::{TransferInterfaceCpi, SplInterfaceCpi};
225/// # use solana_account_info::AccountInfo;
226/// # let source_account: AccountInfo = todo!();
227/// # let destination_account: AccountInfo = todo!();
228/// # let authority: AccountInfo = todo!();
229/// # let payer: AccountInfo = todo!();
230/// # let compressed_token_program_authority: AccountInfo = todo!();
231/// # let system_program: AccountInfo = todo!();
232/// TransferInterfaceCpi::new(
233///     100,    // amount
234///     9,      // decimals
235///     source_account,
236///     destination_account,
237///     authority,
238///     payer,
239///     compressed_token_program_authority,
240///     system_program,
241/// )
242/// .invoke()?;
243/// # Ok::<(), solana_program_error::ProgramError>(())
244/// ```
245pub struct TransferInterfaceCpi<'info> {
246    pub amount: u64,
247    pub decimals: u8,
248    pub source_account: AccountInfo<'info>,
249    pub destination_account: AccountInfo<'info>,
250    pub authority: AccountInfo<'info>,
251    pub payer: AccountInfo<'info>,
252    pub compressed_token_program_authority: AccountInfo<'info>,
253    pub spl_interface: Option<SplInterfaceCpi<'info>>,
254    /// System program - required for compressible account lamport top-ups
255    pub system_program: AccountInfo<'info>,
256}
257
258impl<'info> TransferInterfaceCpi<'info> {
259    /// # Arguments
260    /// * `amount` - Amount to transfer
261    /// * `decimals` - Token decimals (required for SPL transfers)
262    /// * `source_account` - Source token account (can be light or SPL)
263    /// * `destination_account` - Destination token account (can be light or SPL)
264    /// * `authority` - Authority for the transfer (must be signer)
265    /// * `payer` - Payer for the transaction
266    /// * `compressed_token_program_authority` - Compressed token program authority
267    /// * `system_program` - System program (required for compressible account lamport top-ups)
268    #[allow(clippy::too_many_arguments)]
269    pub fn new(
270        amount: u64,
271        decimals: u8,
272        source_account: AccountInfo<'info>,
273        destination_account: AccountInfo<'info>,
274        authority: AccountInfo<'info>,
275        payer: AccountInfo<'info>,
276        compressed_token_program_authority: AccountInfo<'info>,
277        system_program: AccountInfo<'info>,
278    ) -> Self {
279        Self {
280            source_account,
281            destination_account,
282            authority,
283            amount,
284            decimals,
285            payer,
286            compressed_token_program_authority,
287            spl_interface: None,
288            system_program,
289        }
290    }
291
292    /// # Arguments
293    /// * `mint` - Optional mint account (required for SPL<->light transfers)
294    /// * `spl_token_program` - Optional SPL token program (required for SPL<->light transfers)
295    /// * `spl_interface_pda` - Optional SPL interface PDA (required for SPL<->light transfers)
296    /// * `spl_interface_pda_bump` - Optional bump seed for SPL interface PDA
297    pub fn with_spl_interface(
298        mut self,
299        mint: Option<AccountInfo<'info>>,
300        spl_token_program: Option<AccountInfo<'info>>,
301        spl_interface_pda: Option<AccountInfo<'info>>,
302        spl_interface_pda_bump: Option<u8>,
303    ) -> Result<Self, ProgramError> {
304        let mint =
305            mint.ok_or_else(|| ProgramError::Custom(TokenSdkError::MissingMintAccount.into()))?;
306
307        let spl_token_program = spl_token_program
308            .ok_or_else(|| ProgramError::Custom(TokenSdkError::MissingSplTokenProgram.into()))?;
309
310        let spl_interface_pda = spl_interface_pda
311            .ok_or_else(|| ProgramError::Custom(TokenSdkError::MissingSplInterfacePda.into()))?;
312
313        let spl_interface_pda_bump = spl_interface_pda_bump.ok_or_else(|| {
314            ProgramError::Custom(TokenSdkError::MissingSplInterfacePdaBump.into())
315        })?;
316
317        self.spl_interface = Some(SplInterfaceCpi {
318            mint,
319            spl_token_program,
320            spl_interface_pda,
321            spl_interface_pda_bump,
322        });
323        Ok(self)
324    }
325
326    /// Build instruction from CPI context
327    pub fn instruction(&self) -> Result<Instruction, ProgramError> {
328        TransferInterface::from(self).instruction()
329    }
330
331    /// # Errors
332    /// * `SplInterfaceRequired` - If transferring to/from SPL without required accounts
333    /// * `UseRegularSplTransfer` - If both source and destination are SPL accounts
334    pub fn invoke(self) -> Result<(), ProgramError> {
335        use solana_cpi::invoke;
336
337        let transfer_type =
338            determine_transfer_type(self.source_account.owner, self.destination_account.owner)?;
339        let instruction = self.instruction()?;
340
341        match transfer_type {
342            TransferType::LightToLight => {
343                let account_infos = [
344                    self.source_account,
345                    self.destination_account,
346                    self.authority,
347                ];
348                invoke(&instruction, &account_infos)
349            }
350
351            TransferType::LightToSpl => {
352                let config = self.spl_interface.ok_or_else(|| {
353                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
354                })?;
355                let account_infos = [
356                    self.compressed_token_program_authority,
357                    self.payer,
358                    config.mint,
359                    self.source_account,
360                    self.destination_account,
361                    self.authority,
362                    config.spl_interface_pda,
363                    config.spl_token_program,
364                ];
365                invoke(&instruction, &account_infos)
366            }
367
368            TransferType::SplToLight => {
369                let config = self.spl_interface.ok_or_else(|| {
370                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
371                })?;
372                let account_infos = [
373                    self.compressed_token_program_authority,
374                    self.payer,
375                    config.mint,
376                    self.destination_account,
377                    self.authority,
378                    self.source_account,
379                    config.spl_interface_pda,
380                    config.spl_token_program,
381                    self.system_program,
382                ];
383                invoke(&instruction, &account_infos)
384            }
385
386            TransferType::SplToSpl => {
387                let config = self.spl_interface.ok_or_else(|| {
388                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
389                })?;
390                let account_infos = [
391                    self.source_account,
392                    config.mint,
393                    self.destination_account,
394                    self.authority,
395                ];
396                invoke(&instruction, &account_infos)
397            }
398        }
399    }
400
401    /// # Errors
402    /// * `SplInterfaceRequired` - If transferring to/from SPL without required accounts
403    /// * `UseRegularSplTransfer` - If both source and destination are SPL accounts
404    pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
405        use solana_cpi::invoke_signed;
406
407        let transfer_type =
408            determine_transfer_type(self.source_account.owner, self.destination_account.owner)?;
409        let instruction = self.instruction()?;
410
411        match transfer_type {
412            TransferType::LightToLight => {
413                let account_infos = [
414                    self.source_account,
415                    self.destination_account,
416                    self.authority,
417                ];
418                invoke_signed(&instruction, &account_infos, signer_seeds)
419            }
420
421            TransferType::LightToSpl => {
422                let config = self.spl_interface.ok_or_else(|| {
423                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
424                })?;
425                let account_infos = [
426                    self.compressed_token_program_authority,
427                    self.payer,
428                    config.mint,
429                    self.source_account,
430                    self.destination_account,
431                    self.authority,
432                    config.spl_interface_pda,
433                    config.spl_token_program,
434                ];
435                invoke_signed(&instruction, &account_infos, signer_seeds)
436            }
437
438            TransferType::SplToLight => {
439                let config = self.spl_interface.ok_or_else(|| {
440                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
441                })?;
442                let account_infos = [
443                    self.compressed_token_program_authority,
444                    self.payer,
445                    config.mint,
446                    self.destination_account,
447                    self.authority,
448                    self.source_account,
449                    config.spl_interface_pda,
450                    config.spl_token_program,
451                    self.system_program,
452                ];
453                invoke_signed(&instruction, &account_infos, signer_seeds)
454            }
455
456            TransferType::SplToSpl => {
457                let config = self.spl_interface.ok_or_else(|| {
458                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
459                })?;
460                let account_infos = [
461                    self.source_account,
462                    config.mint,
463                    self.destination_account,
464                    self.authority,
465                ];
466                invoke_signed(&instruction, &account_infos, signer_seeds)
467            }
468        }
469    }
470}