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