Skip to main content

light_compressed_token/
spl_compression.rs

1#![allow(deprecated)]
2use anchor_lang::{prelude::*, solana_program::account_info::AccountInfo};
3use anchor_spl::{token::TokenAccount, token_interface};
4
5use crate::{
6    check_spl_token_pool_derivation,
7    constants::{NUM_MAX_POOL_ACCOUNTS, POOL_SEED},
8    process_transfer::{get_cpi_signer_seeds, CompressedTokenInstructionDataTransfer},
9    ErrorCode, TransferInstruction,
10};
11
12pub fn process_compression_or_decompression<'info>(
13    inputs: &CompressedTokenInstructionDataTransfer,
14    ctx: &Context<'_, '_, '_, 'info, TransferInstruction<'info>>,
15) -> Result<()> {
16    if inputs.is_compress {
17        compress_spl_tokens(inputs, ctx)
18    } else {
19        decompress_spl_tokens(inputs, ctx)
20    }
21}
22
23pub fn check_spl_token_pool_derivation_with_index(
24    mint_bytes: &[u8],
25    token_pool_pubkey: &Pubkey,
26    pool_index: &[u8],
27) -> Result<()> {
28    if is_valid_token_pool_pda(mint_bytes, token_pool_pubkey, pool_index, None)? {
29        Ok(())
30    } else {
31        err!(ErrorCode::InvalidTokenPoolPda)
32    }
33}
34
35pub fn is_valid_token_pool_pda(
36    mint_bytes: &[u8],
37    token_pool_pubkey: &Pubkey,
38    pool_index: &[u8],
39    bump: Option<u8>,
40) -> Result<bool> {
41    let pool_index = if pool_index[0] == 0 { &[] } else { pool_index };
42    let pda = if let Some(bump) = bump {
43        let seeds = [POOL_SEED, mint_bytes, pool_index, &[bump]];
44        Pubkey::create_program_address(&seeds[..], &crate::ID)
45            .map_err(|_| crate::ErrorCode::NoMatchingBumpFound)?
46    } else {
47        let seeds = [POOL_SEED, mint_bytes, pool_index];
48        Pubkey::find_program_address(&seeds[..], &crate::ID).0
49    };
50    Ok(pda == *token_pool_pubkey)
51}
52
53pub fn decompress_spl_tokens<'info>(
54    inputs: &CompressedTokenInstructionDataTransfer,
55    ctx: &Context<'_, '_, '_, 'info, TransferInstruction<'info>>,
56) -> Result<()> {
57    let recipient = match ctx.accounts.compress_or_decompress_token_account.as_ref() {
58        Some(compression_recipient) => compression_recipient.to_account_info(),
59        None => return err!(ErrorCode::DecompressRecipientUndefinedForDecompress),
60    };
61    let token_pool_pda = match ctx.accounts.token_pool_pda.as_ref() {
62        Some(token_pool_pda) => token_pool_pda.to_account_info(),
63        None => return err!(ErrorCode::CompressedPdaUndefinedForDecompress),
64    };
65    let amount = match inputs.compress_or_decompress_amount {
66        Some(amount) => amount,
67        None => return err!(ErrorCode::DeCompressAmountUndefinedForDecompress),
68    };
69    invoke_token_program_with_multiple_token_pool_accounts::<false>(
70        ctx.remaining_accounts,
71        &inputs.mint.key().to_bytes(),
72        None,
73        Some(recipient),
74        ctx.accounts.cpi_authority_pda.to_account_info(),
75        ctx.accounts
76            .token_program
77            .as_ref()
78            .unwrap()
79            .to_account_info(),
80        token_pool_pda,
81        amount,
82    )
83}
84
85/// Executes a token program instruction with multiple token pool accounts.
86/// Supported instructions are burn and transfer to decompress spl tokens.
87/// Logic:
88/// 1. Iterate over at most NUM_MAX_POOL_ACCOUNTS token pool accounts.
89/// 2. Start with passed in token pool account.
90/// 3. Determine whether complete amount can be transferred or burned.
91/// 4. Skip if action amount is zero.
92/// 5. Check if the token pool account is derived from the mint.
93/// 6. Return error if the token pool account is not derived
94///    from any combination of mint and bump.
95/// 7. Burn or transfer the amount from the token pool account.
96/// 8. Remove bump from the list of bumps.
97/// 9. Reduce the amount by the transferred or burned amount.
98/// 10. Continue until the amount is zero.
99/// 11. Return if complete amount has been transferred or burned.
100/// 12. Return error if the amount is not zero and the number of accounts has been exhausted.
101#[allow(clippy::too_many_arguments)]
102pub fn invoke_token_program_with_multiple_token_pool_accounts<'info, const IS_BURN: bool>(
103    remaining_accounts: &[AccountInfo<'info>],
104    mint_bytes: &[u8; 32],
105    mint: Option<AccountInfo<'info>>,
106    recipient: Option<AccountInfo<'info>>,
107    cpi_authority_pda: AccountInfo<'info>,
108    token_program: AccountInfo<'info>,
109    mut token_pool_pda: AccountInfo<'info>,
110    mut amount: u64,
111) -> Result<()> {
112    let mut token_pool_indices: Vec<u8> = (0..NUM_MAX_POOL_ACCOUNTS).collect();
113    // 1. iterate over at most NUM_MAX_POOL_ACCOUNTS token pool accounts.
114    for i in 0..NUM_MAX_POOL_ACCOUNTS {
115        // 2. Start with passed in token pool account.token_pool_indices
116        if i != 0 {
117            token_pool_pda = remaining_accounts[i as usize - 1].to_account_info();
118        }
119        let token_pool_amount =
120            TokenAccount::try_deserialize(&mut &token_pool_pda.data.borrow()[..])
121                .map_err(|_| ErrorCode::InvalidTokenPoolPda)?
122                .amount;
123        // 3. Determine whether complete amount can be transferred or burned.
124        let action_amount = std::cmp::min(amount, token_pool_amount);
125        // 4. Skip if action amount is zero.
126        if action_amount == 0 {
127            continue;
128        }
129        // 5. Check if the token pool account is derived from the mint for any bump.
130        for (index, i) in token_pool_indices.iter().enumerate() {
131            if is_valid_token_pool_pda(mint_bytes.as_slice(), &token_pool_pda.key(), &[*i], None)? {
132                // 7. Burn or transfer the amount from the token pool account.
133                if IS_BURN {
134                    crate::burn::spl_burn_cpi(
135                        mint.clone().unwrap(),
136                        cpi_authority_pda.to_account_info(),
137                        token_pool_pda.to_account_info(),
138                        token_program.to_account_info(),
139                        action_amount,
140                        token_pool_amount,
141                    )?;
142                } else {
143                    crate::spl_compression::spl_token_transfer_cpi_with_signer(
144                        token_pool_pda.to_account_info(),
145                        recipient.clone().unwrap(),
146                        cpi_authority_pda.to_account_info(),
147                        token_program.to_account_info(),
148                        action_amount,
149                    )?;
150                }
151                // 8. Remove bump from the list of bumps.
152                token_pool_indices.remove(index);
153                // 9. Reduce the amount by the transferred or burned amount.
154                amount = amount.saturating_sub(action_amount);
155                break;
156            } else if index == token_pool_indices.len() - 1 {
157                // 6. Return error if the token pool account is not derived
158                //      from any combination of mint and bump.
159                return err!(crate::ErrorCode::NoMatchingBumpFound);
160            }
161        }
162
163        // 10. Continue until the amount is zero.
164        // 11. Return if complete amount has been transferred or burned.
165        if amount == 0 {
166            return Ok(());
167        }
168    }
169
170    // 12. return error if the amount is not zero and the number of accounts has been exhausted.
171    msg!("Remaining amount: {}.", amount);
172    if IS_BURN {
173        msg!("Token pool account balances insufficient for burn. \nTry to pass more token pool accounts.");
174        err!(ErrorCode::FailedToBurnSplTokensFromTokenPool)
175    } else {
176        msg!("Token pool account balances insufficient for decompression. \nTry to pass more token pool accounts.");
177        err!(ErrorCode::FailedToDecompress)
178    }
179}
180
181pub fn compress_spl_tokens<'info>(
182    inputs: &CompressedTokenInstructionDataTransfer,
183    ctx: &Context<'_, '_, '_, 'info, TransferInstruction<'info>>,
184) -> Result<()> {
185    let recipient_token_pool = match ctx.accounts.token_pool_pda.as_ref() {
186        Some(token_pool_pda) => token_pool_pda.to_account_info(),
187        None => return err!(ErrorCode::CompressedPdaUndefinedForCompress),
188    };
189    let amount = match inputs.compress_or_decompress_amount {
190        Some(amount) => amount,
191        None => return err!(ErrorCode::DeCompressAmountUndefinedForCompress),
192    };
193
194    check_spl_token_pool_derivation(&recipient_token_pool.key(), &inputs.mint)?;
195    spl_token_transfer(
196        ctx.accounts
197            .compress_or_decompress_token_account
198            .as_ref()
199            .unwrap()
200            .to_account_info(),
201        recipient_token_pool.to_account_info(),
202        ctx.accounts.authority.to_account_info(),
203        ctx.accounts
204            .token_program
205            .as_ref()
206            .unwrap()
207            .to_account_info(),
208        amount,
209    )
210}
211
212/// Invoke the spl token burn instruction with cpi authority pda as signer.
213/// Used to decompress spl tokens.
214pub fn spl_token_transfer_cpi_with_signer<'info>(
215    from: AccountInfo<'info>,
216    to: AccountInfo<'info>,
217    authority: AccountInfo<'info>,
218    token_program: AccountInfo<'info>,
219    amount: u64,
220) -> Result<()> {
221    let signer_seeds = get_cpi_signer_seeds();
222    let signer_seeds_ref = &[&signer_seeds[..]];
223
224    let accounts = token_interface::Transfer {
225        from,
226        to,
227        authority,
228    };
229    let cpi_ctx = CpiContext::new_with_signer(token_program, accounts, signer_seeds_ref);
230    anchor_spl::token_interface::transfer(cpi_ctx, amount)
231}
232
233/// Invoke the spl token transfer instruction with transaction signer.
234/// Used to compress spl tokens.
235pub fn spl_token_transfer<'info>(
236    from: AccountInfo<'info>,
237    to: AccountInfo<'info>,
238    authority: AccountInfo<'info>,
239    token_program: AccountInfo<'info>,
240    amount: u64,
241) -> Result<()> {
242    let instruction = match *token_program.key {
243        spl_token_2022::ID => spl_token_2022::instruction::transfer(
244            token_program.key,
245            from.key,
246            to.key,
247            authority.key,
248            &[],
249            amount,
250        ),
251        spl_token::ID => spl_token::instruction::transfer(
252            token_program.key,
253            from.key,
254            to.key,
255            authority.key,
256            &[],
257            amount,
258        ),
259        _ => return Err(anchor_lang::error::ErrorCode::InvalidProgramId.into()),
260    }?;
261
262    anchor_lang::solana_program::program::invoke(
263        &instruction,
264        &[from, to, authority, token_program],
265    )?;
266    Ok(())
267}