light_compressed_token/
process_transfer.rs

1use account_compression::{utils::constants::CPI_AUTHORITY_PDA_SEED, StateMerkleTreeAccount};
2use anchor_lang::{
3    prelude::*, solana_program::program_error::ProgramError, AnchorDeserialize, Discriminator,
4};
5use light_compressed_account::{
6    compressed_account::{CompressedAccount, CompressedAccountData, PackedMerkleContext},
7    hash_to_bn254_field_size_be,
8    instruction_data::{
9        compressed_proof::CompressedProof,
10        cpi_context::CompressedCpiContext,
11        data::OutputCompressedAccountWithPackedContext,
12        with_readonly::{InAccount, InstructionDataInvokeCpiWithReadOnly},
13    },
14    pubkey::AsPubkey,
15};
16use light_heap::{bench_sbf_end, bench_sbf_start};
17use light_system_program::account_traits::{InvokeAccounts, SignerAccounts};
18use light_zero_copy::num_trait::ZeroCopyNumTrait;
19
20use crate::{
21    constants::{BUMP_CPI_AUTHORITY, NOT_FROZEN, TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR},
22    spl_compression::process_compression_or_decompression,
23    token_data::{AccountState, TokenData},
24    ErrorCode, TransferInstruction,
25};
26
27/// Process a token transfer instruction
28/// build inputs -> sum check -> build outputs -> add token data to inputs -> invoke cpi
29/// 1.  Unpack compressed input accounts and input token data, this uses
30///     standardized signer / delegate and will fail in proof verification in
31///     case either is invalid.
32/// 2.  Check that compressed accounts are of same mint.
33/// 3.  Check that sum of input compressed accounts is equal to sum of output
34///     compressed accounts
35/// 4.  create_output_compressed_accounts
36/// 5.  Serialize and add token_data data to in compressed_accounts.
37/// 6.  Invoke light_system_program::execute_compressed_transaction.
38#[inline(always)]
39pub fn process_transfer<'a, 'b, 'c, 'info: 'b + 'c>(
40    ctx: Context<'a, 'b, 'c, 'info, TransferInstruction<'info>>,
41    inputs: CompressedTokenInstructionDataTransfer,
42) -> Result<()> {
43    bench_sbf_start!("t_context_and_check_sig");
44    if inputs.input_token_data_with_context.is_empty()
45        && inputs.compress_or_decompress_amount.is_none()
46    {
47        return err!(crate::ErrorCode::NoInputTokenAccountsProvided);
48    }
49    let (mut compressed_input_accounts, input_token_data, input_lamports) =
50        get_input_compressed_accounts_with_merkle_context_and_check_signer::<NOT_FROZEN>(
51            &ctx.accounts.authority.key(),
52            &inputs.delegated_transfer,
53            ctx.remaining_accounts,
54            &inputs.input_token_data_with_context,
55            &inputs.mint,
56        )?;
57    bench_sbf_end!("t_context_and_check_sig");
58    bench_sbf_start!("t_sum_check");
59    sum_check(
60        &input_token_data,
61        &inputs
62            .output_compressed_accounts
63            .iter()
64            .map(|data| data.amount)
65            .collect::<Vec<u64>>(),
66        inputs.compress_or_decompress_amount.as_ref(),
67        inputs.is_compress,
68    )?;
69    bench_sbf_end!("t_sum_check");
70    bench_sbf_start!("t_process_compression");
71    if inputs.compress_or_decompress_amount.is_some() {
72        process_compression_or_decompression(&inputs, &ctx)?;
73    }
74    bench_sbf_end!("t_process_compression");
75    bench_sbf_start!("t_create_output_compressed_accounts");
76    let hashed_mint = hash_to_bn254_field_size_be(&inputs.mint.to_bytes());
77
78    let mut output_compressed_accounts = vec![
79        OutputCompressedAccountWithPackedContext::default();
80        inputs.output_compressed_accounts.len()
81    ];
82
83    // If delegate is signer of the transaction determine whether there is a
84    // change account which remains delegated and mark its position.
85    let (is_delegate, delegate) = if let Some(delegated_transfer) = inputs.delegated_transfer {
86        let mut vec = vec![false; inputs.output_compressed_accounts.len()];
87        if let Some(index) = delegated_transfer.delegate_change_account_index {
88            vec[index as usize] = true;
89            (Some(vec), Some(ctx.accounts.authority.key()))
90        } else {
91            (None, None)
92        }
93    } else {
94        (None, None)
95    };
96    inputs.output_compressed_accounts.iter().for_each(|data| {
97        if data.tlv.is_some() {
98            unimplemented!("Tlv is unimplemented");
99        }
100    });
101    let output_lamports = create_output_compressed_accounts(
102        &mut output_compressed_accounts,
103        inputs.mint,
104        inputs
105            .output_compressed_accounts
106            .iter()
107            .map(|data| data.owner)
108            .collect::<Vec<Pubkey>>()
109            .as_slice(),
110        delegate,
111        is_delegate,
112        inputs
113            .output_compressed_accounts
114            .iter()
115            .map(|data: &PackedTokenTransferOutputData| data.amount)
116            .collect::<Vec<u64>>()
117            .as_slice(),
118        Some(
119            inputs
120                .output_compressed_accounts
121                .iter()
122                .map(|data: &PackedTokenTransferOutputData| data.lamports)
123                .collect::<Vec<Option<u64>>>(),
124        ),
125        &hashed_mint,
126        &inputs
127            .output_compressed_accounts
128            .iter()
129            .map(|data| data.merkle_tree_index)
130            .collect::<Vec<u8>>(),
131        ctx.remaining_accounts,
132    )?;
133    bench_sbf_end!("t_create_output_compressed_accounts");
134
135    bench_sbf_start!("t_add_token_data_to_input_compressed_accounts");
136    if !compressed_input_accounts.is_empty() {
137        add_data_hash_to_input_compressed_accounts::<false>(
138            &mut compressed_input_accounts,
139            input_token_data.as_slice(),
140            &hashed_mint,
141            ctx.remaining_accounts,
142        )?;
143    }
144    bench_sbf_end!("t_add_token_data_to_input_compressed_accounts");
145
146    // If input and output lamports are unbalanced create a change account
147    // without token data.
148    let change_lamports = input_lamports - output_lamports;
149    if change_lamports > 0 {
150        let new_len = output_compressed_accounts.len() + 1;
151        // Resize vector to new_len so that no unnecessary memory is allocated.
152        // (Rust doubles the size of the vector when pushing to a full vector.)
153        output_compressed_accounts.resize(
154            new_len,
155            OutputCompressedAccountWithPackedContext {
156                compressed_account: CompressedAccount {
157                    owner: ctx.accounts.authority.key().into(),
158                    lamports: change_lamports,
159                    data: None,
160                    address: None,
161                },
162                merkle_tree_index: inputs.output_compressed_accounts[0].merkle_tree_index,
163            },
164        );
165    }
166
167    cpi_execute_compressed_transaction_transfer(
168        ctx.accounts,
169        compressed_input_accounts,
170        output_compressed_accounts,
171        inputs.with_transaction_hash,
172        inputs.proof,
173        inputs.cpi_context,
174        ctx.accounts.cpi_authority_pda.to_account_info(),
175        ctx.accounts.light_system_program.to_account_info(),
176        ctx.accounts.self_program.to_account_info(),
177        ctx.remaining_accounts,
178    )
179}
180pub const BATCHED_DISCRIMINATOR: &[u8] = b"BatchMta";
181pub const OUTPUT_QUEUE_DISCRIMINATOR: &[u8] = b"queueacc";
182
183/// Creates output compressed accounts.
184/// Steps:
185/// 1. Allocate memory for token data.
186/// 2. Create, hash and serialize token data.
187/// 3. Create compressed account data.
188/// 4. Repeat for every pubkey.
189#[allow(clippy::too_many_arguments)]
190pub fn create_output_compressed_accounts(
191    output_compressed_accounts: &mut [OutputCompressedAccountWithPackedContext],
192    mint_pubkey: impl AsPubkey,
193    pubkeys: &[impl AsPubkey],
194    delegate: Option<Pubkey>,
195    is_delegate: Option<Vec<bool>>,
196    amounts: &[impl ZeroCopyNumTrait],
197    lamports: Option<Vec<Option<impl ZeroCopyNumTrait>>>,
198    hashed_mint: &[u8; 32],
199    merkle_tree_indices: &[u8],
200    remaining_accounts: &[AccountInfo<'_>],
201) -> Result<u64> {
202    let mut sum_lamports = 0;
203    let hashed_delegate_store = if let Some(delegate) = delegate {
204        hash_to_bn254_field_size_be(delegate.to_bytes().as_slice())
205    } else {
206        [0u8; 32]
207    };
208    for (i, (owner, amount)) in pubkeys.iter().zip(amounts.iter()).enumerate() {
209        let (delegate, hashed_delegate) = if is_delegate
210            .as_ref()
211            .map(|is_delegate| is_delegate[i])
212            .unwrap_or(false)
213        {
214            (
215                delegate.as_ref().map(|delegate_pubkey| *delegate_pubkey),
216                Some(&hashed_delegate_store),
217            )
218        } else {
219            (None, None)
220        };
221        // 107/75 =
222        //      32      mint
223        // +    32      owner
224        // +    8       amount
225        // +    1 + 32  option + delegate (optional)
226        // +    1       state
227        // +    1       tlv (None)
228        let capacity = if delegate.is_some() { 107 } else { 75 };
229        let mut token_data_bytes = Vec::with_capacity(capacity);
230        // 1,000 CU token data and serialize
231        let token_data = TokenData {
232            mint: (mint_pubkey).to_anchor_pubkey(),
233            owner: (*owner).to_anchor_pubkey(),
234            amount: (*amount).into(),
235            delegate,
236            state: AccountState::Initialized,
237            tlv: None,
238        };
239        // TODO: remove serialization, just write bytes.
240        token_data.serialize(&mut token_data_bytes).unwrap();
241        bench_sbf_start!("token_data_hash");
242        let hashed_owner = hash_to_bn254_field_size_be(owner.to_pubkey_bytes().as_slice());
243
244        let mut amount_bytes = [0u8; 32];
245        let discriminator_bytes =
246            &remaining_accounts[merkle_tree_indices[i] as usize].try_borrow_data()?[0..8];
247        match discriminator_bytes {
248            StateMerkleTreeAccount::DISCRIMINATOR => {
249                amount_bytes[24..].copy_from_slice(amount.to_bytes_le().as_slice());
250                Ok(())
251            }
252            BATCHED_DISCRIMINATOR => {
253                amount_bytes[24..].copy_from_slice(amount.to_bytes_be().as_slice());
254                Ok(())
255            }
256            OUTPUT_QUEUE_DISCRIMINATOR => {
257                amount_bytes[24..].copy_from_slice(amount.to_bytes_be().as_slice());
258                Ok(())
259            }
260            _ => {
261                msg!(
262                    "{} is no Merkle tree or output queue account. ",
263                    remaining_accounts[merkle_tree_indices[i] as usize].key()
264                );
265                err!(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch)
266            }
267        }?;
268
269        let data_hash = TokenData::hash_with_hashed_values(
270            hashed_mint,
271            &hashed_owner,
272            &amount_bytes,
273            &hashed_delegate,
274        )
275        .map_err(ProgramError::from)?;
276        let data = CompressedAccountData {
277            discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR,
278            data: token_data_bytes,
279            data_hash,
280        };
281
282        bench_sbf_end!("token_data_hash");
283        let lamports = lamports
284            .as_ref()
285            .and_then(|lamports| lamports[i])
286            .unwrap_or(0u64.into());
287        sum_lamports += lamports.into();
288        output_compressed_accounts[i] = OutputCompressedAccountWithPackedContext {
289            compressed_account: CompressedAccount {
290                owner: crate::ID.into(),
291                lamports: lamports.into(),
292                data: Some(data),
293                address: None,
294            },
295            merkle_tree_index: merkle_tree_indices[i],
296        };
297    }
298    Ok(sum_lamports)
299}
300
301/// Create input compressed account data hash
302/// 1. enforces discriminator
303/// 2. hashes token data
304/// 3. actual data is not needed for input compressed accounts
305pub fn add_data_hash_to_input_compressed_accounts<const FROZEN_INPUTS: bool>(
306    input_compressed_accounts_with_merkle_context: &mut [InAccount],
307    input_token_data: &[TokenData],
308    hashed_mint: &[u8; 32],
309    remaining_accounts: &[AccountInfo<'_>],
310) -> Result<()> {
311    for (i, compressed_account_with_context) in input_compressed_accounts_with_merkle_context
312        .iter_mut()
313        .enumerate()
314    {
315        let hashed_owner = hash_to_bn254_field_size_be(&input_token_data[i].owner.to_bytes());
316
317        let mut amount_bytes = [0u8; 32];
318        let discriminator_bytes = &remaining_accounts[compressed_account_with_context
319            .merkle_context
320            .merkle_tree_pubkey_index
321            as usize]
322            .try_borrow_data()?[0..8];
323        match discriminator_bytes {
324            StateMerkleTreeAccount::DISCRIMINATOR => {
325                amount_bytes[24..]
326                    .copy_from_slice(input_token_data[i].amount.to_le_bytes().as_slice());
327                Ok(())
328            }
329            BATCHED_DISCRIMINATOR => {
330                amount_bytes[24..]
331                    .copy_from_slice(input_token_data[i].amount.to_be_bytes().as_slice());
332                Ok(())
333            }
334            OUTPUT_QUEUE_DISCRIMINATOR => {
335                amount_bytes[24..]
336                    .copy_from_slice(input_token_data[i].amount.to_be_bytes().as_slice());
337                Ok(())
338            }
339            _ => {
340                msg!(
341                    "{} is no Merkle tree or output queue account. ",
342                    remaining_accounts[compressed_account_with_context
343                        .merkle_context
344                        .merkle_tree_pubkey_index as usize]
345                        .key()
346                );
347                err!(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch)
348            }
349        }?;
350        let delegate_store;
351        let hashed_delegate = if let Some(delegate) = input_token_data[i].delegate {
352            delegate_store = hash_to_bn254_field_size_be(&delegate.to_bytes());
353            Some(&delegate_store)
354        } else {
355            None
356        };
357        compressed_account_with_context.data_hash = if !FROZEN_INPUTS {
358            TokenData::hash_with_hashed_values(
359                hashed_mint,
360                &hashed_owner,
361                &amount_bytes,
362                &hashed_delegate,
363            )
364            .map_err(ProgramError::from)?
365        } else {
366            TokenData::hash_frozen_with_hashed_values(
367                hashed_mint,
368                &hashed_owner,
369                &amount_bytes,
370                &hashed_delegate,
371            )
372            .map_err(ProgramError::from)?
373        };
374    }
375    Ok(())
376}
377
378/// Get static cpi signer seeds
379pub fn get_cpi_signer_seeds() -> [&'static [u8]; 2] {
380    let bump: &[u8; 1] = &[BUMP_CPI_AUTHORITY];
381    let seeds: [&'static [u8]; 2] = [CPI_AUTHORITY_PDA_SEED, bump];
382    seeds
383}
384
385#[inline(never)]
386#[allow(clippy::too_many_arguments)]
387pub fn cpi_execute_compressed_transaction_transfer<
388    'info,
389    A: InvokeAccounts<'info> + SignerAccounts<'info>,
390>(
391    ctx: &A,
392    input_compressed_accounts: Vec<InAccount>,
393    output_compressed_accounts: Vec<OutputCompressedAccountWithPackedContext>,
394    with_transaction_hash: bool,
395    proof: Option<CompressedProof>,
396    cpi_context: Option<CompressedCpiContext>,
397    cpi_authority_pda: AccountInfo<'info>,
398    _system_program_account_info: AccountInfo<'info>,
399    _invoking_program_account_info: AccountInfo<'info>,
400    remaining_accounts: &[AccountInfo<'info>],
401) -> Result<()> {
402    bench_sbf_start!("t_cpi_prep");
403
404    let signer_seeds = get_cpi_signer_seeds();
405    let signer_seeds_ref = &[&signer_seeds[..]];
406
407    let cpi_context_account = cpi_context.map(|cpi_context| {
408        remaining_accounts[cpi_context.cpi_context_account_index as usize].to_account_info()
409    });
410
411    #[cfg(not(feature = "cpi-without-program-ids"))]
412    let mode = 0;
413    #[cfg(feature = "cpi-without-program-ids")]
414    let mode = 1;
415    let inputs_struct = InstructionDataInvokeCpiWithReadOnly {
416        mode,
417        bump: BUMP_CPI_AUTHORITY,
418        invoking_program_id: crate::ID.into(),
419        with_cpi_context: cpi_context.is_some(),
420        cpi_context: cpi_context.unwrap_or_default(),
421        with_transaction_hash,
422        read_only_accounts: Vec::new(),
423        read_only_addresses: Vec::new(),
424        input_compressed_accounts,
425        output_compressed_accounts,
426        proof,
427        new_address_params: Vec::new(),
428        compress_or_decompress_lamports: 0,
429        is_compress: false,
430    };
431
432    #[cfg(not(feature = "cpi-without-program-ids"))]
433    {
434        let cpi_accounts = light_system_program::cpi::accounts::InvokeCpiInstruction {
435            fee_payer: ctx.get_fee_payer().to_account_info(),
436            authority: cpi_authority_pda,
437            registered_program_pda: ctx.get_registered_program_pda().to_account_info(),
438            noop_program: ctx.get_noop_program().to_account_info(),
439            account_compression_authority: ctx
440                .get_account_compression_authority()
441                .to_account_info(),
442            account_compression_program: ctx.get_account_compression_program().to_account_info(),
443            invoking_program: _invoking_program_account_info,
444            system_program: ctx.get_system_program().to_account_info(),
445            sol_pool_pda: None,
446            decompression_recipient: None,
447            cpi_context_account,
448        };
449        let mut cpi_ctx = CpiContext::new_with_signer(
450            _system_program_account_info,
451            cpi_accounts,
452            signer_seeds_ref,
453        );
454
455        cpi_ctx.remaining_accounts = remaining_accounts.to_vec();
456        bench_sbf_end!("t_cpi_prep");
457
458        bench_sbf_start!("t_invoke_cpi");
459        light_system_program::cpi::invoke_cpi_with_read_only(cpi_ctx, inputs_struct)?;
460        bench_sbf_end!("t_invoke_cpi");
461    }
462    #[cfg(feature = "cpi-without-program-ids")]
463    {
464        let mut inputs = Vec::new();
465        InstructionDataInvokeCpiWithReadOnly::serialize(&inputs_struct, &mut inputs)
466            .map_err(ProgramError::from)?;
467
468        let mut data = Vec::with_capacity(8 + inputs.len());
469        data.extend_from_slice(
470            &light_compressed_account::discriminators::DISCRIMINATOR_INVOKE_CPI_WITH_READ_ONLY,
471        );
472        data.extend(inputs);
473
474        // 4 static accounts
475        let accounts_len = 4 + remaining_accounts.len() + cpi_context.is_some() as usize;
476        let mut account_infos = Vec::with_capacity(accounts_len);
477        let mut account_metas = Vec::with_capacity(accounts_len);
478        account_infos.push(ctx.get_fee_payer().to_account_info());
479        account_infos.push(cpi_authority_pda);
480        account_infos.push(ctx.get_registered_program_pda().to_account_info());
481        account_infos.push(ctx.get_account_compression_authority().to_account_info());
482
483        account_metas.push(AccountMeta {
484            pubkey: account_infos[0].key(),
485            is_signer: true,
486            is_writable: true,
487        });
488        account_metas.push(AccountMeta {
489            pubkey: account_infos[1].key(),
490            is_signer: true,
491            is_writable: false,
492        });
493        account_metas.push(AccountMeta {
494            pubkey: account_infos[2].key(),
495            is_signer: false,
496            is_writable: false,
497        });
498        account_metas.push(AccountMeta {
499            pubkey: account_infos[3].key(),
500            is_signer: false,
501            is_writable: false,
502        });
503        let mut remaining_accounts_index = 4;
504
505        if let Some(account_info) = cpi_context_account {
506            account_infos.push(account_info);
507            account_metas.push(AccountMeta {
508                pubkey: account_infos[remaining_accounts_index].key(),
509                is_signer: false,
510                is_writable: true,
511            });
512            remaining_accounts_index += 1;
513        }
514        for account_info in remaining_accounts {
515            account_infos.push(account_info.clone());
516            account_metas.push(AccountMeta {
517                pubkey: account_infos[remaining_accounts_index].key(),
518                is_signer: false,
519                is_writable: account_infos[remaining_accounts_index].is_writable,
520            });
521            remaining_accounts_index += 1;
522        }
523
524        let instruction = anchor_lang::solana_program::instruction::Instruction {
525            program_id: light_system_program::ID,
526            accounts: account_metas,
527            data,
528        };
529
530        anchor_lang::solana_program::program::invoke_signed(
531            &instruction,
532            account_infos.as_slice(),
533            signer_seeds_ref.as_slice(),
534        )?;
535    }
536    Ok(())
537}
538
539pub fn sum_check(
540    input_token_data_elements: &[TokenData],
541    output_amounts: &[u64],
542    compress_or_decompress_amount: Option<&u64>,
543    is_compress: bool,
544) -> Result<()> {
545    let mut sum: u64 = 0;
546    for input_token_data in input_token_data_elements.iter() {
547        sum = sum
548            .checked_add(input_token_data.amount)
549            .ok_or(ProgramError::ArithmeticOverflow)
550            .map_err(|_| ErrorCode::ComputeInputSumFailed)?;
551    }
552
553    if let Some(compress_or_decompress_amount) = compress_or_decompress_amount {
554        if is_compress {
555            sum = sum
556                .checked_add(*compress_or_decompress_amount)
557                .ok_or(ProgramError::ArithmeticOverflow)
558                .map_err(|_| ErrorCode::ComputeCompressSumFailed)?;
559        } else {
560            sum = sum
561                .checked_sub(*compress_or_decompress_amount)
562                .ok_or(ProgramError::ArithmeticOverflow)
563                .map_err(|_| ErrorCode::ComputeDecompressSumFailed)?;
564        }
565    }
566
567    for amount in output_amounts.iter() {
568        sum = sum
569            .checked_sub(*amount)
570            .ok_or(ProgramError::ArithmeticOverflow)
571            .map_err(|_| ErrorCode::ComputeOutputSumFailed)?;
572    }
573
574    if sum == 0 {
575        Ok(())
576    } else {
577        Err(ErrorCode::SumCheckFailed.into())
578    }
579}
580
581#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)]
582pub struct InputTokenDataWithContext {
583    pub amount: u64,
584    pub delegate_index: Option<u8>,
585    pub merkle_context: PackedMerkleContext,
586    pub root_index: u16,
587    pub lamports: Option<u64>,
588    /// Placeholder for TokenExtension tlv data (unimplemented)
589    pub tlv: Option<Vec<u8>>,
590}
591
592/// Struct to provide the owner when the delegate is signer of the transaction.
593#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)]
594pub struct DelegatedTransfer {
595    pub owner: Pubkey,
596    /// Index of change compressed account in output compressed accounts. In
597    /// case that the delegate didn't spend the complete delegated compressed
598    /// account balance the change compressed account will be delegated to her
599    /// as well.
600    pub delegate_change_account_index: Option<u8>,
601}
602
603#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)]
604pub struct CompressedTokenInstructionDataTransfer {
605    pub proof: Option<CompressedProof>,
606    pub mint: Pubkey,
607    /// Is required if the signer is delegate,
608    /// -> delegate is authority account,
609    /// owner = Some(owner) is the owner of the token account.
610    pub delegated_transfer: Option<DelegatedTransfer>,
611    pub input_token_data_with_context: Vec<InputTokenDataWithContext>,
612    pub output_compressed_accounts: Vec<PackedTokenTransferOutputData>,
613    pub is_compress: bool,
614    pub compress_or_decompress_amount: Option<u64>,
615    pub cpi_context: Option<CompressedCpiContext>,
616    pub lamports_change_account_merkle_tree_index: Option<u8>,
617    pub with_transaction_hash: bool,
618}
619
620pub fn get_input_compressed_accounts_with_merkle_context_and_check_signer<const IS_FROZEN: bool>(
621    signer: &Pubkey,
622    signer_is_delegate: &Option<DelegatedTransfer>,
623    remaining_accounts: &[AccountInfo<'_>],
624    input_token_data_with_context: &[InputTokenDataWithContext],
625    mint: &Pubkey,
626) -> Result<(Vec<InAccount>, Vec<TokenData>, u64)> {
627    // Collect the total number of lamports to check whether inputs and outputs
628    // are unbalanced. If unbalanced create a non token compressed change
629    // account owner by the sender.
630    let mut sum_lamports = 0;
631    let mut input_compressed_accounts_with_merkle_context: Vec<InAccount> =
632        Vec::<InAccount>::with_capacity(input_token_data_with_context.len());
633    let mut input_token_data_vec: Vec<TokenData> =
634        Vec::with_capacity(input_token_data_with_context.len());
635
636    for input_token_data in input_token_data_with_context.iter() {
637        let owner = if input_token_data.delegate_index.is_none() {
638            *signer
639        } else if let Some(signer_is_delegate) = signer_is_delegate {
640            signer_is_delegate.owner
641        } else {
642            *signer
643        };
644        // This is a check for convenience to throw a meaningful error.
645        // The actual security results from the proof verification.
646        if signer_is_delegate.is_some()
647            && input_token_data.delegate_index.is_some()
648            && *signer
649                != remaining_accounts[input_token_data.delegate_index.unwrap() as usize].key()
650        {
651            msg!(
652                "signer {:?} != delegate in remaining accounts {:?}",
653                signer,
654                remaining_accounts[input_token_data.delegate_index.unwrap() as usize].key()
655            );
656            msg!(
657                "delegate index {:?}",
658                input_token_data.delegate_index.unwrap() as usize
659            );
660            return err!(ErrorCode::DelegateSignerCheckFailed);
661        }
662
663        let compressed_account = InAccount {
664            lamports: input_token_data.lamports.unwrap_or_default(),
665            discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR,
666            merkle_context: input_token_data.merkle_context,
667            root_index: input_token_data.root_index,
668            data_hash: [0u8; 32],
669            address: None,
670        };
671        sum_lamports += compressed_account.lamports;
672        let state = if IS_FROZEN {
673            AccountState::Frozen
674        } else {
675            AccountState::Initialized
676        };
677        if input_token_data.tlv.is_some() {
678            unimplemented!("Tlv is unimplemented.");
679        }
680        let token_data = TokenData {
681            mint: *mint,
682            owner,
683            amount: input_token_data.amount,
684            delegate: input_token_data.delegate_index.map(|_| {
685                remaining_accounts[input_token_data.delegate_index.unwrap() as usize].key()
686            }),
687            state,
688            tlv: None,
689        };
690        input_token_data_vec.push(token_data);
691        input_compressed_accounts_with_merkle_context.push(compressed_account);
692    }
693    Ok((
694        input_compressed_accounts_with_merkle_context,
695        input_token_data_vec,
696        sum_lamports,
697    ))
698}
699
700#[derive(Clone, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)]
701pub struct PackedTokenTransferOutputData {
702    pub owner: Pubkey,
703    pub amount: u64,
704    pub lamports: Option<u64>,
705    pub merkle_tree_index: u8,
706    /// Placeholder for TokenExtension tlv data (unimplemented)
707    pub tlv: Option<Vec<u8>>,
708}
709
710#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)]
711pub struct TokenTransferOutputData {
712    pub owner: Pubkey,
713    pub amount: u64,
714    pub lamports: Option<u64>,
715    pub merkle_tree: Pubkey,
716}
717
718pub fn get_cpi_authority_pda() -> (Pubkey, u8) {
719    Pubkey::find_program_address(&[CPI_AUTHORITY_PDA_SEED], &crate::ID)
720}
721
722#[cfg(not(target_os = "solana"))]
723pub mod transfer_sdk {
724    use std::collections::HashMap;
725
726    use anchor_lang::{error_code, AnchorSerialize, Id, InstructionData, ToAccountMetas};
727    use anchor_spl::{token::Token, token_2022::Token2022};
728    use light_compressed_account::{
729        compressed_account::{CompressedAccount, MerkleContext, PackedMerkleContext},
730        instruction_data::compressed_proof::CompressedProof,
731    };
732    use solana_sdk::{
733        instruction::{AccountMeta, Instruction},
734        pubkey::Pubkey,
735    };
736
737    use super::{
738        DelegatedTransfer, InputTokenDataWithContext, PackedTokenTransferOutputData,
739        TokenTransferOutputData,
740    };
741    use crate::{token_data::TokenData, CompressedTokenInstructionDataTransfer};
742
743    #[error_code]
744    pub enum TransferSdkError {
745        #[msg("Signer check failed")]
746        SignerCheckFailed,
747        #[msg("Create transfer instruction failed")]
748        CreateTransferInstructionFailed,
749        #[msg("Account not found")]
750        AccountNotFound,
751        #[msg("Serialization error")]
752        SerializationError,
753    }
754
755    #[allow(clippy::too_many_arguments)]
756    pub fn create_transfer_instruction(
757        fee_payer: &Pubkey,
758        owner: &Pubkey,
759        input_merkle_context: &[MerkleContext],
760        output_compressed_accounts: &[TokenTransferOutputData],
761        root_indices: &[Option<u16>],
762        proof: &Option<CompressedProof>,
763        input_token_data: &[TokenData],
764        input_compressed_accounts: &[CompressedAccount],
765        mint: Pubkey,
766        delegate: Option<Pubkey>,
767        is_compress: bool,
768        compress_or_decompress_amount: Option<u64>,
769        token_pool_pda: Option<Pubkey>,
770        compress_or_decompress_token_account: Option<Pubkey>,
771        sort: bool,
772        delegate_change_account_index: Option<u8>,
773        lamports_change_account_merkle_tree: Option<Pubkey>,
774        is_token_22: bool,
775        additional_token_pools: &[Pubkey],
776        with_transaction_hash: bool,
777    ) -> Result<Instruction, TransferSdkError> {
778        let (remaining_accounts, mut inputs_struct) = create_inputs_and_remaining_accounts(
779            input_token_data,
780            input_compressed_accounts,
781            input_merkle_context,
782            delegate,
783            output_compressed_accounts,
784            root_indices,
785            proof,
786            mint,
787            is_compress,
788            compress_or_decompress_amount,
789            delegate_change_account_index,
790            lamports_change_account_merkle_tree,
791            additional_token_pools,
792            with_transaction_hash,
793        );
794        if sort {
795            inputs_struct
796                .output_compressed_accounts
797                .sort_by_key(|data| data.merkle_tree_index);
798        }
799        let remaining_accounts = to_account_metas(remaining_accounts);
800        let mut inputs = Vec::new();
801        CompressedTokenInstructionDataTransfer::serialize(&inputs_struct, &mut inputs)
802            .map_err(|_| TransferSdkError::SerializationError)?;
803
804        let (cpi_authority_pda, _) = crate::process_transfer::get_cpi_authority_pda();
805        let instruction_data = crate::instruction::Transfer { inputs }.data();
806        let authority = if let Some(delegate) = delegate {
807            delegate
808        } else {
809            *owner
810        };
811        let token_program = if compress_or_decompress_token_account.is_none() {
812            None
813        } else if is_token_22 {
814            Some(Token2022::id())
815        } else {
816            Some(Token::id())
817        };
818
819        let accounts = crate::accounts::TransferInstruction {
820            fee_payer: *fee_payer,
821            authority,
822            cpi_authority_pda,
823            light_system_program: light_system_program::ID,
824            registered_program_pda: light_system_program::utils::get_registered_program_pda(
825                &light_system_program::ID,
826            ),
827            noop_program: Pubkey::new_from_array(
828                account_compression::utils::constants::NOOP_PUBKEY,
829            ),
830            account_compression_authority: light_system_program::utils::get_cpi_authority_pda(
831                &light_system_program::ID,
832            ),
833            account_compression_program: account_compression::ID,
834            self_program: crate::ID,
835            token_pool_pda,
836            compress_or_decompress_token_account,
837            token_program,
838            system_program: solana_sdk::system_program::ID,
839        };
840
841        Ok(Instruction {
842            program_id: crate::ID,
843            accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(),
844
845            data: instruction_data,
846        })
847    }
848
849    #[allow(clippy::too_many_arguments)]
850    pub fn create_inputs_and_remaining_accounts_checked(
851        input_token_data: &[TokenData],
852        input_compressed_accounts: &[CompressedAccount],
853        input_merkle_context: &[MerkleContext],
854        owner_if_delegate_is_signer: Option<Pubkey>,
855        output_compressed_accounts: &[TokenTransferOutputData],
856        root_indices: &[Option<u16>],
857        proof: &Option<CompressedProof>,
858        mint: Pubkey,
859        owner: &Pubkey,
860        is_compress: bool,
861        compress_or_decompress_amount: Option<u64>,
862        delegate_change_account_index: Option<u8>,
863        lamports_change_account_merkle_tree: Option<Pubkey>,
864    ) -> Result<
865        (
866            HashMap<Pubkey, usize>,
867            CompressedTokenInstructionDataTransfer,
868        ),
869        TransferSdkError,
870    > {
871        for token_data in input_token_data {
872            // convenience signer check to throw a meaningful error
873            if token_data.owner != *owner {
874                println!(
875                    "owner: {:?}, token_data.owner: {:?}",
876                    owner, token_data.owner
877                );
878                return Err(TransferSdkError::SignerCheckFailed);
879            }
880        }
881        let (remaining_accounts, compressed_accounts_ix_data) =
882            create_inputs_and_remaining_accounts(
883                input_token_data,
884                input_compressed_accounts,
885                input_merkle_context,
886                owner_if_delegate_is_signer,
887                output_compressed_accounts,
888                root_indices,
889                proof,
890                mint,
891                is_compress,
892                compress_or_decompress_amount,
893                delegate_change_account_index,
894                lamports_change_account_merkle_tree,
895                &[],
896                false,
897            );
898        Ok((remaining_accounts, compressed_accounts_ix_data))
899    }
900
901    #[allow(clippy::too_many_arguments)]
902    pub fn create_inputs_and_remaining_accounts(
903        input_token_data: &[TokenData],
904        input_compressed_accounts: &[CompressedAccount],
905        input_merkle_context: &[MerkleContext],
906        delegate: Option<Pubkey>,
907        output_compressed_accounts: &[TokenTransferOutputData],
908        root_indices: &[Option<u16>],
909        proof: &Option<CompressedProof>,
910        mint: Pubkey,
911        is_compress: bool,
912        compress_or_decompress_amount: Option<u64>,
913        delegate_change_account_index: Option<u8>,
914        lamports_change_account_merkle_tree: Option<Pubkey>,
915        accounts: &[Pubkey],
916        with_transaction_hash: bool,
917    ) -> (
918        HashMap<Pubkey, usize>,
919        CompressedTokenInstructionDataTransfer,
920    ) {
921        let mut additional_accounts = Vec::new();
922        additional_accounts.extend_from_slice(accounts);
923        if let Some(delegate) = delegate {
924            additional_accounts.push(delegate);
925            for account in input_token_data.iter() {
926                if account.delegate.is_some() && delegate != account.delegate.unwrap() {
927                    println!("delegate: {:?}", delegate);
928                    println!("account.delegate: {:?}", account.delegate.unwrap());
929                    panic!("Delegate is not the same as the signer");
930                }
931            }
932        }
933        let lamports_change_account_merkle_tree_index = if let Some(
934            lamports_change_account_merkle_tree,
935        ) = lamports_change_account_merkle_tree
936        {
937            additional_accounts.push(lamports_change_account_merkle_tree);
938            Some(additional_accounts.len() as u8 - 1)
939        } else {
940            None
941        };
942        let (remaining_accounts, input_token_data_with_context, _output_compressed_accounts) =
943            create_input_output_and_remaining_accounts(
944                additional_accounts.as_slice(),
945                input_token_data,
946                input_compressed_accounts,
947                input_merkle_context,
948                root_indices,
949                output_compressed_accounts,
950            );
951        let delegated_transfer = if delegate.is_some() {
952            let delegated_transfer = DelegatedTransfer {
953                owner: input_token_data[0].owner,
954                delegate_change_account_index,
955            };
956            Some(delegated_transfer)
957        } else {
958            None
959        };
960        let inputs_struct = CompressedTokenInstructionDataTransfer {
961            output_compressed_accounts: _output_compressed_accounts.to_vec(),
962            proof: *proof,
963            input_token_data_with_context,
964            delegated_transfer,
965            mint,
966            is_compress,
967            compress_or_decompress_amount,
968            cpi_context: None,
969            lamports_change_account_merkle_tree_index,
970            with_transaction_hash,
971        };
972
973        (remaining_accounts, inputs_struct)
974    }
975
976    pub fn create_input_output_and_remaining_accounts(
977        additional_accounts: &[Pubkey],
978        input_token_data: &[TokenData],
979        input_compressed_accounts: &[CompressedAccount],
980        input_merkle_context: &[MerkleContext],
981        root_indices: &[Option<u16>],
982        output_compressed_accounts: &[TokenTransferOutputData],
983    ) -> (
984        HashMap<Pubkey, usize>,
985        Vec<InputTokenDataWithContext>,
986        Vec<PackedTokenTransferOutputData>,
987    ) {
988        let mut remaining_accounts = HashMap::<Pubkey, usize>::new();
989
990        let mut index = 0;
991        for account in additional_accounts {
992            match remaining_accounts.get(account) {
993                Some(_) => {}
994                None => {
995                    remaining_accounts.insert(*account, index);
996                    index += 1;
997                }
998            };
999        }
1000        let mut input_token_data_with_context: Vec<InputTokenDataWithContext> = Vec::new();
1001
1002        for (i, token_data) in input_token_data.iter().enumerate() {
1003            match remaining_accounts.get(&input_merkle_context[i].merkle_tree_pubkey.into()) {
1004                Some(_) => {}
1005                None => {
1006                    remaining_accounts
1007                        .insert(input_merkle_context[i].merkle_tree_pubkey.into(), index);
1008                    index += 1;
1009                }
1010            };
1011            let delegate_index = match token_data.delegate {
1012                Some(delegate) => match remaining_accounts.get(&delegate) {
1013                    Some(delegate_index) => Some(*delegate_index as u8),
1014                    None => {
1015                        remaining_accounts.insert(delegate, index);
1016                        index += 1;
1017                        Some((index - 1) as u8)
1018                    }
1019                },
1020                None => None,
1021            };
1022            let lamports = if input_compressed_accounts[i].lamports != 0 {
1023                Some(input_compressed_accounts[i].lamports)
1024            } else {
1025                None
1026            };
1027            // Potential footgun queue index is set in merkle tree but its not used here
1028            let prove_by_index = root_indices[i].is_none();
1029
1030            let token_data_with_context = InputTokenDataWithContext {
1031                amount: token_data.amount,
1032                delegate_index,
1033                merkle_context: PackedMerkleContext {
1034                    merkle_tree_pubkey_index: *remaining_accounts
1035                        .get(&input_merkle_context[i].merkle_tree_pubkey.into())
1036                        .unwrap() as u8,
1037                    queue_pubkey_index: 0,
1038                    leaf_index: input_merkle_context[i].leaf_index,
1039                    prove_by_index,
1040                },
1041                root_index: root_indices[i].unwrap_or_default(),
1042                lamports,
1043                tlv: None,
1044            };
1045            input_token_data_with_context.push(token_data_with_context);
1046        }
1047        for (i, _) in input_token_data.iter().enumerate() {
1048            match remaining_accounts.get(&input_merkle_context[i].queue_pubkey.into()) {
1049                Some(_) => {}
1050                None => {
1051                    remaining_accounts.insert(input_merkle_context[i].queue_pubkey.into(), index);
1052                    index += 1;
1053                }
1054            };
1055            input_token_data_with_context[i]
1056                .merkle_context
1057                .queue_pubkey_index = *remaining_accounts
1058                .get(&input_merkle_context[i].queue_pubkey.into())
1059                .unwrap() as u8;
1060        }
1061        let mut _output_compressed_accounts: Vec<PackedTokenTransferOutputData> =
1062            Vec::with_capacity(output_compressed_accounts.len());
1063        for (i, mt) in output_compressed_accounts.iter().enumerate() {
1064            match remaining_accounts.get(&mt.merkle_tree) {
1065                Some(_) => {}
1066                None => {
1067                    remaining_accounts.insert(mt.merkle_tree, index);
1068                    index += 1;
1069                }
1070            };
1071            _output_compressed_accounts.push(PackedTokenTransferOutputData {
1072                owner: output_compressed_accounts[i].owner,
1073                amount: output_compressed_accounts[i].amount,
1074                lamports: output_compressed_accounts[i].lamports,
1075                merkle_tree_index: *remaining_accounts.get(&mt.merkle_tree).unwrap() as u8,
1076                tlv: None,
1077            });
1078        }
1079        (
1080            remaining_accounts,
1081            input_token_data_with_context,
1082            _output_compressed_accounts,
1083        )
1084    }
1085
1086    pub fn to_account_metas(remaining_accounts: HashMap<Pubkey, usize>) -> Vec<AccountMeta> {
1087        let mut remaining_accounts = remaining_accounts
1088            .iter()
1089            .map(|(k, i)| {
1090                (
1091                    AccountMeta {
1092                        pubkey: *k,
1093                        is_signer: false,
1094                        is_writable: true,
1095                    },
1096                    *i,
1097                )
1098            })
1099            .collect::<Vec<(AccountMeta, usize)>>();
1100        // hash maps are not sorted so we need to sort manually and collect into a vector again
1101        remaining_accounts.sort_by_key(|(_, idx)| *idx);
1102        let remaining_accounts = remaining_accounts
1103            .iter()
1104            .map(|(k, _)| k.clone())
1105            .collect::<Vec<AccountMeta>>();
1106        remaining_accounts
1107    }
1108}
1109
1110#[cfg(test)]
1111mod test {
1112    use super::*;
1113    use crate::token_data::AccountState;
1114
1115    #[test]
1116    fn test_sum_check() {
1117        // SUCCEED: no relay fee, compression
1118        sum_check_test(&[100, 50], &[150], None, false).unwrap();
1119        sum_check_test(&[75, 25, 25], &[25, 25, 25, 25, 12, 13], None, false).unwrap();
1120
1121        // FAIL: no relay fee, compression
1122        sum_check_test(&[100, 50], &[150 + 1], None, false).unwrap_err();
1123        sum_check_test(&[100, 50], &[150 - 1], None, false).unwrap_err();
1124        sum_check_test(&[100, 50], &[], None, false).unwrap_err();
1125        sum_check_test(&[], &[100, 50], None, false).unwrap_err();
1126
1127        // SUCCEED: empty
1128        sum_check_test(&[], &[], None, true).unwrap();
1129        sum_check_test(&[], &[], None, false).unwrap();
1130        // FAIL: empty
1131        sum_check_test(&[], &[], Some(1), false).unwrap_err();
1132        sum_check_test(&[], &[], Some(1), true).unwrap_err();
1133
1134        // SUCCEED: with compress
1135        sum_check_test(&[100], &[123], Some(23), true).unwrap();
1136        sum_check_test(&[], &[150], Some(150), true).unwrap();
1137        // FAIL: compress
1138        sum_check_test(&[], &[150], Some(150 - 1), true).unwrap_err();
1139        sum_check_test(&[], &[150], Some(150 + 1), true).unwrap_err();
1140
1141        // SUCCEED: with decompress
1142        sum_check_test(&[100, 50], &[100], Some(50), false).unwrap();
1143        sum_check_test(&[100, 50], &[], Some(150), false).unwrap();
1144        // FAIL: decompress
1145        sum_check_test(&[100, 50], &[], Some(150 - 1), false).unwrap_err();
1146        sum_check_test(&[100, 50], &[], Some(150 + 1), false).unwrap_err();
1147    }
1148
1149    fn sum_check_test(
1150        input_amounts: &[u64],
1151        output_amounts: &[u64],
1152        compress_or_decompress_amount: Option<u64>,
1153        is_compress: bool,
1154    ) -> Result<()> {
1155        let mut inputs = Vec::new();
1156        for i in input_amounts.iter() {
1157            inputs.push(TokenData {
1158                mint: Pubkey::new_unique(),
1159                owner: Pubkey::new_unique(),
1160                delegate: None,
1161                state: AccountState::Initialized,
1162                amount: *i,
1163                tlv: None,
1164            });
1165        }
1166        let ref_amount;
1167        let compress_or_decompress_amount = match compress_or_decompress_amount {
1168            Some(amount) => {
1169                ref_amount = amount;
1170                Some(&ref_amount)
1171            }
1172            None => None,
1173        };
1174        sum_check(
1175            inputs.as_slice(),
1176            output_amounts,
1177            compress_or_decompress_amount,
1178            is_compress,
1179        )
1180    }
1181}