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                fee_payer: None,
139            }
140            .instruction(),
141
142            TransferType::LightToSpl => {
143                let spl = self.spl_interface.ok_or_else(|| {
144                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
145                })?;
146                TransferToSpl {
147                    source: self.source,
148                    destination_spl_token_account: self.destination,
149                    amount: self.amount,
150                    authority: self.authority,
151                    mint: spl.mint,
152                    payer: self.payer,
153                    spl_interface_pda: spl.spl_interface_pda,
154                    spl_interface_pda_bump: spl.spl_interface_pda_bump,
155                    decimals: self.decimals,
156                    spl_token_program: spl.spl_token_program,
157                }
158                .instruction()
159            }
160
161            TransferType::SplToLight => {
162                let spl = self.spl_interface.ok_or_else(|| {
163                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
164                })?;
165                TransferFromSpl {
166                    source_spl_token_account: self.source,
167                    destination: self.destination,
168                    amount: self.amount,
169                    authority: self.authority,
170                    mint: spl.mint,
171                    payer: self.payer,
172                    spl_interface_pda: spl.spl_interface_pda,
173                    spl_interface_pda_bump: spl.spl_interface_pda_bump,
174                    decimals: self.decimals,
175                    spl_token_program: spl.spl_token_program,
176                }
177                .instruction()
178            }
179
180            TransferType::SplToSpl => {
181                let spl = self.spl_interface.ok_or_else(|| {
182                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
183                })?;
184
185                // Build SPL transfer_checked instruction manually
186                // Discriminator 12 = TransferChecked
187                let mut data = vec![12u8];
188                data.extend_from_slice(&self.amount.to_le_bytes());
189                data.push(self.decimals);
190
191                Ok(Instruction {
192                    program_id: self.source_owner, // SPL token program
193                    accounts: vec![
194                        AccountMeta::new(self.source, false),
195                        AccountMeta::new_readonly(spl.mint, false),
196                        AccountMeta::new(self.destination, false),
197                        AccountMeta::new_readonly(self.authority, true),
198                    ],
199                    data,
200                })
201            }
202        }
203    }
204}
205
206impl<'info> From<&TransferInterfaceCpi<'info>> for TransferInterface {
207    fn from(cpi: &TransferInterfaceCpi<'info>) -> Self {
208        Self {
209            source: *cpi.source_account.key,
210            destination: *cpi.destination_account.key,
211            amount: cpi.amount,
212            decimals: cpi.decimals,
213            authority: *cpi.authority.key,
214            payer: *cpi.payer.key,
215            spl_interface: cpi.spl_interface.as_ref().map(SplInterface::from),
216            max_top_up: None,
217            source_owner: *cpi.source_account.owner,
218            destination_owner: *cpi.destination_account.owner,
219        }
220    }
221}
222
223/// # Transfer interface via CPI (auto-detects account types):
224/// ```rust,no_run
225/// # use light_token::instruction::{TransferInterfaceCpi, SplInterfaceCpi};
226/// # use solana_account_info::AccountInfo;
227/// # let source_account: AccountInfo = todo!();
228/// # let destination_account: AccountInfo = todo!();
229/// # let authority: AccountInfo = todo!();
230/// # let payer: AccountInfo = todo!();
231/// # let compressed_token_program_authority: AccountInfo = todo!();
232/// # let system_program: AccountInfo = todo!();
233/// TransferInterfaceCpi::new(
234///     100,    // amount
235///     9,      // decimals
236///     source_account,
237///     destination_account,
238///     authority,
239///     payer,
240///     compressed_token_program_authority,
241///     system_program,
242/// )
243/// .invoke()?;
244/// # Ok::<(), solana_program_error::ProgramError>(())
245/// ```
246pub struct TransferInterfaceCpi<'info> {
247    pub amount: u64,
248    pub decimals: u8,
249    pub source_account: AccountInfo<'info>,
250    pub destination_account: AccountInfo<'info>,
251    pub authority: AccountInfo<'info>,
252    pub payer: AccountInfo<'info>,
253    pub compressed_token_program_authority: AccountInfo<'info>,
254    pub spl_interface: Option<SplInterfaceCpi<'info>>,
255    /// System program - required for compressible account lamport top-ups
256    pub system_program: AccountInfo<'info>,
257}
258
259impl<'info> TransferInterfaceCpi<'info> {
260    /// # Arguments
261    /// * `amount` - Amount to transfer
262    /// * `decimals` - Token decimals (required for SPL transfers)
263    /// * `source_account` - Source token account (can be light or SPL)
264    /// * `destination_account` - Destination token account (can be light or SPL)
265    /// * `authority` - Authority for the transfer (must be signer)
266    /// * `payer` - Payer for the transaction
267    /// * `compressed_token_program_authority` - Compressed token program authority
268    /// * `system_program` - System program (required for compressible account lamport top-ups)
269    #[allow(clippy::too_many_arguments)]
270    pub fn new(
271        amount: u64,
272        decimals: u8,
273        source_account: AccountInfo<'info>,
274        destination_account: AccountInfo<'info>,
275        authority: AccountInfo<'info>,
276        payer: AccountInfo<'info>,
277        compressed_token_program_authority: AccountInfo<'info>,
278        system_program: AccountInfo<'info>,
279    ) -> Self {
280        Self {
281            source_account,
282            destination_account,
283            authority,
284            amount,
285            decimals,
286            payer,
287            compressed_token_program_authority,
288            spl_interface: None,
289            system_program,
290        }
291    }
292
293    /// # Arguments
294    /// * `mint` - Optional mint account (required for SPL<->light transfers)
295    /// * `spl_token_program` - Optional SPL token program (required for SPL<->light transfers)
296    /// * `spl_interface_pda` - Optional SPL interface PDA (required for SPL<->light transfers)
297    /// * `spl_interface_pda_bump` - Optional bump seed for SPL interface PDA
298    pub fn with_spl_interface(
299        mut self,
300        mint: Option<AccountInfo<'info>>,
301        spl_token_program: Option<AccountInfo<'info>>,
302        spl_interface_pda: Option<AccountInfo<'info>>,
303        spl_interface_pda_bump: Option<u8>,
304    ) -> Result<Self, ProgramError> {
305        let mint =
306            mint.ok_or_else(|| ProgramError::Custom(TokenSdkError::MissingMintAccount.into()))?;
307
308        let spl_token_program = spl_token_program
309            .ok_or_else(|| ProgramError::Custom(TokenSdkError::MissingSplTokenProgram.into()))?;
310
311        let spl_interface_pda = spl_interface_pda
312            .ok_or_else(|| ProgramError::Custom(TokenSdkError::MissingSplInterfacePda.into()))?;
313
314        let spl_interface_pda_bump = spl_interface_pda_bump.ok_or_else(|| {
315            ProgramError::Custom(TokenSdkError::MissingSplInterfacePdaBump.into())
316        })?;
317
318        self.spl_interface = Some(SplInterfaceCpi {
319            mint,
320            spl_token_program,
321            spl_interface_pda,
322            spl_interface_pda_bump,
323        });
324        Ok(self)
325    }
326
327    /// Build instruction from CPI context
328    pub fn instruction(&self) -> Result<Instruction, ProgramError> {
329        TransferInterface::from(self).instruction()
330    }
331
332    /// # Errors
333    /// * `SplInterfaceRequired` - If transferring to/from SPL without required accounts
334    /// * `UseRegularSplTransfer` - If both source and destination are SPL accounts
335    pub fn invoke(self) -> Result<(), ProgramError> {
336        use solana_cpi::invoke;
337
338        let transfer_type =
339            determine_transfer_type(self.source_account.owner, self.destination_account.owner)?;
340        let instruction = self.instruction()?;
341
342        match transfer_type {
343            TransferType::LightToLight => {
344                let account_infos = [
345                    self.source_account,
346                    self.destination_account,
347                    self.authority,
348                ];
349                invoke(&instruction, &account_infos)
350            }
351
352            TransferType::LightToSpl => {
353                let config = self.spl_interface.ok_or_else(|| {
354                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
355                })?;
356                let account_infos = [
357                    self.compressed_token_program_authority,
358                    self.payer,
359                    config.mint,
360                    self.source_account,
361                    self.destination_account,
362                    self.authority,
363                    config.spl_interface_pda,
364                    config.spl_token_program,
365                ];
366                invoke(&instruction, &account_infos)
367            }
368
369            TransferType::SplToLight => {
370                let config = self.spl_interface.ok_or_else(|| {
371                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
372                })?;
373                let account_infos = [
374                    self.compressed_token_program_authority,
375                    self.payer,
376                    config.mint,
377                    self.destination_account,
378                    self.authority,
379                    self.source_account,
380                    config.spl_interface_pda,
381                    config.spl_token_program,
382                    self.system_program,
383                ];
384                invoke(&instruction, &account_infos)
385            }
386
387            TransferType::SplToSpl => {
388                let config = self.spl_interface.ok_or_else(|| {
389                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
390                })?;
391                let account_infos = [
392                    self.source_account,
393                    config.mint,
394                    self.destination_account,
395                    self.authority,
396                ];
397                invoke(&instruction, &account_infos)
398            }
399        }
400    }
401
402    /// # Errors
403    /// * `SplInterfaceRequired` - If transferring to/from SPL without required accounts
404    /// * `UseRegularSplTransfer` - If both source and destination are SPL accounts
405    pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
406        use solana_cpi::invoke_signed;
407
408        let transfer_type =
409            determine_transfer_type(self.source_account.owner, self.destination_account.owner)?;
410        let instruction = self.instruction()?;
411
412        match transfer_type {
413            TransferType::LightToLight => {
414                let account_infos = [
415                    self.source_account,
416                    self.destination_account,
417                    self.authority,
418                ];
419                invoke_signed(&instruction, &account_infos, signer_seeds)
420            }
421
422            TransferType::LightToSpl => {
423                let config = self.spl_interface.ok_or_else(|| {
424                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
425                })?;
426                let account_infos = [
427                    self.compressed_token_program_authority,
428                    self.payer,
429                    config.mint,
430                    self.source_account,
431                    self.destination_account,
432                    self.authority,
433                    config.spl_interface_pda,
434                    config.spl_token_program,
435                ];
436                invoke_signed(&instruction, &account_infos, signer_seeds)
437            }
438
439            TransferType::SplToLight => {
440                let config = self.spl_interface.ok_or_else(|| {
441                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
442                })?;
443                let account_infos = [
444                    self.compressed_token_program_authority,
445                    self.payer,
446                    config.mint,
447                    self.destination_account,
448                    self.authority,
449                    self.source_account,
450                    config.spl_interface_pda,
451                    config.spl_token_program,
452                    self.system_program,
453                ];
454                invoke_signed(&instruction, &account_infos, signer_seeds)
455            }
456
457            TransferType::SplToSpl => {
458                let config = self.spl_interface.ok_or_else(|| {
459                    ProgramError::Custom(TokenSdkError::SplInterfaceRequired.into())
460                })?;
461                let account_infos = [
462                    self.source_account,
463                    config.mint,
464                    self.destination_account,
465                    self.authority,
466                ];
467                invoke_signed(&instruction, &account_infos, signer_seeds)
468            }
469        }
470    }
471}