Skip to main content

light_token/instruction/
transfer_from_spl.rs

1use light_compressed_account::instruction_data::compressed_proof::ValidityProof;
2use light_compressed_token_sdk::compressed_token::{
3    transfer2::{
4        create_transfer2_instruction, Transfer2AccountsMetaConfig, Transfer2Config, Transfer2Inputs,
5    },
6    CTokenAccount2,
7};
8use light_token_interface::instructions::transfer2::{Compression, MultiTokenTransferOutputData};
9use solana_account_info::AccountInfo;
10use solana_cpi::{invoke, invoke_signed};
11use solana_instruction::{AccountMeta, Instruction};
12use solana_program_error::ProgramError;
13use solana_pubkey::Pubkey;
14
15/// # Create a transfer SPL to cToken instruction
16/// ```rust
17/// # use solana_pubkey::Pubkey;
18/// # use light_token::instruction::TransferFromSpl;
19/// # let source_spl_token_account = Pubkey::new_unique();
20/// # let destination = Pubkey::new_unique();
21/// # let authority = Pubkey::new_unique();
22/// # let mint = Pubkey::new_unique();
23/// # let payer = Pubkey::new_unique();
24/// # let spl_interface_pda = Pubkey::new_unique();
25/// # let spl_token_program = Pubkey::new_unique();
26/// let instruction = TransferFromSpl {
27///     amount: 100,
28///     spl_interface_pda_bump: 255,
29///     decimals: 9,
30///     source_spl_token_account,
31///     destination,
32///     authority,
33///     mint,
34///     payer,
35///     spl_interface_pda,
36///     spl_token_program,
37/// }.instruction()?;
38/// # Ok::<(), solana_program_error::ProgramError>(())
39/// ```
40pub struct TransferFromSpl {
41    pub amount: u64,
42    pub spl_interface_pda_bump: u8,
43    pub decimals: u8,
44    pub source_spl_token_account: Pubkey,
45    /// Destination ctoken account (writable)
46    pub destination: Pubkey,
47    pub authority: Pubkey,
48    pub mint: Pubkey,
49    pub payer: Pubkey,
50    pub spl_interface_pda: Pubkey,
51    pub spl_token_program: Pubkey,
52}
53
54/// # Transfer SPL to ctoken via CPI:
55/// ```rust,no_run
56/// # use light_token::instruction::TransferFromSplCpi;
57/// # use solana_account_info::AccountInfo;
58/// # let source_spl_token_account: AccountInfo = todo!();
59/// # let destination: AccountInfo = todo!();
60/// # let authority: AccountInfo = todo!();
61/// # let mint: AccountInfo = todo!();
62/// # let payer: AccountInfo = todo!();
63/// # let spl_interface_pda: AccountInfo = todo!();
64/// # let spl_token_program: AccountInfo = todo!();
65/// # let compressed_token_program_authority: AccountInfo = todo!();
66/// # let system_program: AccountInfo = todo!();
67/// TransferFromSplCpi {
68///     amount: 100,
69///     spl_interface_pda_bump: 255,
70///     decimals: 9,
71///     source_spl_token_account,
72///     destination,
73///     authority,
74///     mint,
75///     payer,
76///     spl_interface_pda,
77///     spl_token_program,
78///     compressed_token_program_authority,
79///     system_program,
80/// }
81/// .invoke()?;
82/// # Ok::<(), solana_program_error::ProgramError>(())
83/// ```
84pub struct TransferFromSplCpi<'info> {
85    pub amount: u64,
86    pub spl_interface_pda_bump: u8,
87    pub decimals: u8,
88    pub source_spl_token_account: AccountInfo<'info>,
89    /// Destination ctoken account (writable)
90    pub destination: AccountInfo<'info>,
91    pub authority: AccountInfo<'info>,
92    pub mint: AccountInfo<'info>,
93    pub payer: AccountInfo<'info>,
94    pub spl_interface_pda: AccountInfo<'info>,
95    pub spl_token_program: AccountInfo<'info>,
96    pub compressed_token_program_authority: AccountInfo<'info>,
97    /// System program - required for compressible account lamport top-ups
98    pub system_program: AccountInfo<'info>,
99}
100
101impl<'info> TransferFromSplCpi<'info> {
102    pub fn instruction(&self) -> Result<Instruction, ProgramError> {
103        TransferFromSpl::from(self).instruction()
104    }
105
106    pub fn invoke(self) -> Result<(), ProgramError> {
107        let instruction = TransferFromSpl::from(&self).instruction()?;
108        // Account order must match instruction metas: cpi_authority_pda, fee_payer, packed_accounts...
109        let account_infos = [
110            self.compressed_token_program_authority, // CPI authority PDA (first)
111            self.payer,                              // Fee payer (second)
112            self.mint,                               // Index 0: Mint
113            self.destination,                        // Index 1: Destination ctoken account
114            self.authority,                          // Index 2: Authority (signer)
115            self.source_spl_token_account,           // Index 3: Source SPL token account
116            self.spl_interface_pda,                  // Index 4: SPL interface PDA
117            self.spl_token_program,                  // Index 5: SPL Token program
118            self.system_program,                     // Index 6: System program
119        ];
120        invoke(&instruction, &account_infos)
121    }
122
123    pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> {
124        let instruction = TransferFromSpl::from(&self).instruction()?;
125        // Account order must match instruction metas: cpi_authority_pda, fee_payer, packed_accounts...
126        let account_infos = [
127            self.compressed_token_program_authority, // CPI authority PDA (first)
128            self.payer,                              // Fee payer (second)
129            self.mint,                               // Index 0: Mint
130            self.destination,                        // Index 1: Destination ctoken account
131            self.authority,                          // Index 2: Authority (signer)
132            self.source_spl_token_account,           // Index 3: Source SPL token account
133            self.spl_interface_pda,                  // Index 4: SPL interface PDA
134            self.spl_token_program,                  // Index 5: SPL Token program
135            self.system_program,                     // Index 6: System program
136        ];
137        invoke_signed(&instruction, &account_infos, signer_seeds)
138    }
139}
140
141impl<'info> From<&TransferFromSplCpi<'info>> for TransferFromSpl {
142    fn from(account_infos: &TransferFromSplCpi<'info>) -> Self {
143        Self {
144            source_spl_token_account: *account_infos.source_spl_token_account.key,
145            destination: *account_infos.destination.key,
146            amount: account_infos.amount,
147            authority: *account_infos.authority.key,
148            mint: *account_infos.mint.key,
149            payer: *account_infos.payer.key,
150            spl_interface_pda: *account_infos.spl_interface_pda.key,
151            spl_interface_pda_bump: account_infos.spl_interface_pda_bump,
152            decimals: account_infos.decimals,
153            spl_token_program: *account_infos.spl_token_program.key,
154        }
155    }
156}
157
158impl TransferFromSpl {
159    pub fn instruction(self) -> Result<Instruction, ProgramError> {
160        let packed_accounts = vec![
161            // Mint (index 0)
162            AccountMeta::new_readonly(self.mint, false),
163            // Destination ctoken account (index 1) - writable
164            AccountMeta::new(self.destination, false),
165            // Authority for compression (index 2) - signer
166            AccountMeta::new_readonly(self.authority, true),
167            // Source SPL token account (index 3) - writable
168            AccountMeta::new(self.source_spl_token_account, false),
169            // SPL interface PDA (index 4) - writable
170            AccountMeta::new(self.spl_interface_pda, false),
171            // SPL Token program (index 5) - needed for CPI
172            AccountMeta::new_readonly(self.spl_token_program, false),
173            // System program (index 6) - needed for compressible account lamport top-ups
174            AccountMeta::new_readonly(Pubkey::default(), false),
175        ];
176
177        let wrap_from_spl = CTokenAccount2 {
178            inputs: vec![],
179            output: MultiTokenTransferOutputData::default(),
180            compression: Some(Compression::compress_spl(
181                self.amount,
182                0, // mint
183                3, // source or recipient
184                2, // authority
185                4, // pool_account_index:
186                0, // pool_index
187                self.spl_interface_pda_bump,
188                self.decimals,
189            )),
190            delegate_is_set: false,
191            method_used: true,
192        };
193
194        let unwrap_to_destination = CTokenAccount2 {
195            inputs: vec![],
196            output: MultiTokenTransferOutputData::default(),
197            compression: Some(Compression::decompress(self.amount, 0, 1)),
198            delegate_is_set: false,
199            method_used: true,
200        };
201
202        let inputs = Transfer2Inputs {
203            validity_proof: ValidityProof::new(None),
204            transfer_config: Transfer2Config::default().filter_zero_amount_outputs(),
205            meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only(
206                self.payer,
207                packed_accounts,
208            ),
209            in_lamports: None,
210            out_lamports: None,
211            token_accounts: vec![wrap_from_spl, unwrap_to_destination],
212            output_queue: 0, // Decompressed accounts only, no output queue needed
213            in_tlv: None,
214        };
215
216        create_transfer2_instruction(inputs).map_err(ProgramError::from)
217    }
218}