Skip to main content

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