light_token/instruction/
transfer_to_spl.rs

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