Skip to main content

light_token/instruction/
transfer_interface.rs

1use light_compressed_token_sdk::utils::is_light_token_owner;
2use solana_account_info::AccountInfo;
3use solana_instruction::{AccountMeta, Instruction};
4use solana_program_error::ProgramError;
5use solana_pubkey::Pubkey;
6
7use super::{
8    transfer::Transfer, transfer_from_spl::TransferFromSpl, transfer_to_spl::TransferToSpl,
9};
10use crate::error::LightTokenError;
11
12/// Internal enum to classify transfer types based on account owners.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14enum TransferType {
15    /// light -> light
16    LightToLight,
17    /// light -> SPL (decompress)
18    LightToSpl,
19    /// SPL -> light (compress)
20    SplToLight,
21    /// SPL -> SPL (pass-through to SPL token program)
22    SplToSpl,
23}
24
25/// Determine transfer type from account owners.
26///
27/// Returns `Ok(TransferType)` if at least one account is a Light token account.
28/// Returns `Err(UseRegularSplTransfer)` if both accounts are non-Light (SPL) accounts.
29/// Returns `Err(CannotDetermineAccountType)` if an account owner is unrecognized.
30fn determine_transfer_type(
31    source_owner: &Pubkey,
32    destination_owner: &Pubkey,
33) -> Result<TransferType, ProgramError> {
34    let source_is_light = is_light_token_owner(source_owner)
35        .map_err(|_| ProgramError::Custom(LightTokenError::CannotDetermineAccountType.into()))?;
36    let dest_is_light = is_light_token_owner(destination_owner)
37        .map_err(|_| ProgramError::Custom(LightTokenError::CannotDetermineAccountType.into()))?;
38
39    match (source_is_light, dest_is_light) {
40        (true, true) => Ok(TransferType::LightToLight),
41        (true, false) => Ok(TransferType::LightToSpl),
42        (false, true) => Ok(TransferType::SplToLight),
43        (false, false) => {
44            // Both are SPL - verify same token program
45            if source_owner == destination_owner {
46                Ok(TransferType::SplToSpl)
47            } else {
48                Err(ProgramError::Custom(
49                    LightTokenError::SplTokenProgramMismatch.into(),
50                ))
51            }
52        }
53    }
54}
55
56/// Required accounts to interface between light and SPL token accounts (Pubkey-based).
57///
58/// Use this struct when building instructions outside of CPI context.
59#[derive(Debug, Clone, Copy)]
60pub struct SplInterface {
61    pub mint: Pubkey,
62    pub spl_token_program: Pubkey,
63    pub spl_interface_pda: Pubkey,
64    pub spl_interface_pda_bump: u8,
65}
66
67impl<'info> From<&SplInterfaceCpi<'info>> for SplInterface {
68    fn from(spl: &SplInterfaceCpi<'info>) -> Self {
69        Self {
70            mint: *spl.mint.key,
71            spl_token_program: *spl.spl_token_program.key,
72            spl_interface_pda: *spl.spl_interface_pda.key,
73            spl_interface_pda_bump: spl.spl_interface_pda_bump,
74        }
75    }
76}
77
78/// Required accounts to interface between light and SPL token accounts (AccountInfo-based).
79///
80/// Use this struct when building CPIs.
81pub struct SplInterfaceCpi<'info> {
82    pub mint: AccountInfo<'info>,
83    pub spl_token_program: AccountInfo<'info>,
84    pub spl_interface_pda: AccountInfo<'info>,
85    pub spl_interface_pda_bump: u8,
86}
87
88/// # Create a transfer interface instruction that auto-routes based on account types:
89/// ```rust
90/// # use solana_pubkey::Pubkey;
91/// # use light_token::instruction::{TransferInterface, SplInterface, LIGHT_TOKEN_PROGRAM_ID};
92/// # let source = Pubkey::new_unique();
93/// # let destination = Pubkey::new_unique();
94/// # let authority = Pubkey::new_unique();
95/// # let payer = Pubkey::new_unique();
96/// // For light -> light transfer (source_owner and destination_owner are LIGHT_TOKEN_PROGRAM_ID)
97/// let instruction = TransferInterface {
98///     source,
99///     destination,
100///     amount: 100,
101///     decimals: 9,
102///     authority,
103///     payer,
104///     spl_interface: None,
105///     max_top_up: None,
106///     source_owner: LIGHT_TOKEN_PROGRAM_ID,
107///     destination_owner: LIGHT_TOKEN_PROGRAM_ID,
108/// }.instruction()?;
109/// # Ok::<(), solana_program_error::ProgramError>(())
110/// ```
111pub struct TransferInterface {
112    pub source: Pubkey,
113    pub destination: Pubkey,
114    pub amount: u64,
115    pub decimals: u8,
116    pub authority: Pubkey,
117    pub payer: Pubkey,
118    pub spl_interface: Option<SplInterface>,
119    /// Maximum lamports for rent and top-up combined (for light->light transfers)
120    pub max_top_up: Option<u16>,
121    /// Owner of the source account (used to determine transfer type)
122    pub source_owner: Pubkey,
123    /// Owner of the destination account (used to determine transfer type)
124    pub destination_owner: Pubkey,
125}
126
127impl TransferInterface {
128    /// Build instruction based on detected transfer type
129    pub fn instruction(self) -> Result<Instruction, ProgramError> {
130        match determine_transfer_type(&self.source_owner, &self.destination_owner)? {
131            TransferType::LightToLight => Transfer {
132                source: self.source,
133                destination: self.destination,
134                amount: self.amount,
135                authority: self.authority,
136                max_top_up: self.max_top_up,
137                fee_payer: None,
138            }
139            .instruction(),
140
141            TransferType::LightToSpl => {
142                let spl = self.spl_interface.ok_or_else(|| {
143                    ProgramError::Custom(LightTokenError::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(LightTokenError::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(LightTokenError::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` - Light 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(LightTokenError::MissingMintAccount.into()))?;
306
307        let spl_token_program = spl_token_program
308            .ok_or_else(|| ProgramError::Custom(LightTokenError::MissingSplTokenProgram.into()))?;
309
310        let spl_interface_pda = spl_interface_pda
311            .ok_or_else(|| ProgramError::Custom(LightTokenError::MissingSplInterfacePda.into()))?;
312
313        let spl_interface_pda_bump = spl_interface_pda_bump.ok_or_else(|| {
314            ProgramError::Custom(LightTokenError::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(LightTokenError::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(LightTokenError::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(LightTokenError::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(LightTokenError::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(LightTokenError::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(LightTokenError::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}