light_compressed_token/
process_transfer.rs

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