light_compressed_token/
burn.rs

1use anchor_lang::prelude::*;
2use anchor_spl::token::TokenAccount;
3use light_compressed_account::{
4    hash_to_bn254_field_size_be,
5    instruction_data::{
6        compressed_proof::CompressedProof, cpi_context::CompressedCpiContext,
7        data::OutputCompressedAccountWithPackedContext, with_readonly::InAccount,
8    },
9};
10
11use crate::{
12    constants::NOT_FROZEN,
13    process_transfer::{
14        add_data_hash_to_input_compressed_accounts, cpi_execute_compressed_transaction_transfer,
15        create_output_compressed_accounts, get_cpi_signer_seeds,
16        get_input_compressed_accounts_with_merkle_context_and_check_signer, DelegatedTransfer,
17        InputTokenDataWithContext,
18    },
19    spl_compression::invoke_token_program_with_multiple_token_pool_accounts,
20    BurnInstruction, ErrorCode,
21};
22
23#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)]
24pub struct CompressedTokenInstructionDataBurn {
25    pub proof: CompressedProof,
26    pub input_token_data_with_context: Vec<InputTokenDataWithContext>,
27    pub cpi_context: Option<CompressedCpiContext>,
28    pub burn_amount: u64,
29    pub change_account_merkle_tree_index: u8,
30    pub delegated_transfer: Option<DelegatedTransfer>,
31}
32
33pub fn process_burn<'a, 'b, 'c, 'info: 'b + 'c>(
34    ctx: Context<'a, 'b, 'c, 'info, BurnInstruction<'info>>,
35    inputs: Vec<u8>,
36) -> Result<()> {
37    let inputs: CompressedTokenInstructionDataBurn =
38        CompressedTokenInstructionDataBurn::deserialize(&mut inputs.as_slice())?;
39    burn_spl_from_pool_pda(&ctx, &inputs)?;
40    let mint = ctx.accounts.mint.key();
41    let (compressed_input_accounts, output_compressed_accounts) =
42        create_input_and_output_accounts_burn(
43            &inputs,
44            &ctx.accounts.authority.key(),
45            ctx.remaining_accounts,
46            &mint,
47        )?;
48    let proof = if inputs.proof == CompressedProof::default() {
49        None
50    } else {
51        Some(inputs.proof)
52    };
53    cpi_execute_compressed_transaction_transfer(
54        ctx.accounts,
55        compressed_input_accounts,
56        output_compressed_accounts,
57        false,
58        proof,
59        inputs.cpi_context,
60        ctx.accounts.cpi_authority_pda.to_account_info(),
61        ctx.accounts.light_system_program.to_account_info(),
62        ctx.accounts.self_program.to_account_info(),
63        ctx.remaining_accounts,
64    )?;
65    Ok(())
66}
67
68#[inline(never)]
69pub fn burn_spl_from_pool_pda<'info>(
70    ctx: &Context<'_, '_, '_, 'info, BurnInstruction<'info>>,
71    inputs: &CompressedTokenInstructionDataBurn,
72) -> Result<()> {
73    let amount = inputs.burn_amount;
74    let token_pool_pda = &ctx.accounts.token_pool_pda;
75
76    invoke_token_program_with_multiple_token_pool_accounts::<true>(
77        ctx.remaining_accounts,
78        &ctx.accounts.mint.key().to_bytes(),
79        Some(ctx.accounts.mint.to_account_info()),
80        None,
81        ctx.accounts.cpi_authority_pda.to_account_info(),
82        ctx.accounts.token_program.to_account_info(),
83        token_pool_pda.to_account_info(),
84        amount,
85    )
86}
87
88pub fn spl_burn_cpi<'info>(
89    mint: AccountInfo<'info>,
90    cpi_authority_pda: AccountInfo<'info>,
91    token_pool_pda: AccountInfo<'info>,
92    token_program: AccountInfo<'info>,
93    burn_amount: u64,
94    pre_token_balance: u64,
95) -> Result<()> {
96    let cpi_accounts = anchor_spl::token_interface::Burn {
97        mint,
98        from: token_pool_pda.to_account_info(),
99        authority: cpi_authority_pda,
100    };
101    let signer_seeds = get_cpi_signer_seeds();
102    let signer_seeds_ref = &[&signer_seeds[..]];
103    let cpi_ctx = CpiContext::new_with_signer(token_program, cpi_accounts, signer_seeds_ref);
104    anchor_spl::token_interface::burn(cpi_ctx, burn_amount)?;
105    let post_token_balance =
106        TokenAccount::try_deserialize(&mut &token_pool_pda.data.borrow()[..])?.amount;
107    if post_token_balance != pre_token_balance - burn_amount {
108        msg!(
109            "post_token_balance {} != pre_token_balance {} - burn_amount {}",
110            post_token_balance,
111            pre_token_balance,
112            burn_amount
113        );
114        return err!(crate::ErrorCode::SplTokenSupplyMismatch);
115    }
116    Ok(())
117}
118
119pub fn create_input_and_output_accounts_burn(
120    inputs: &CompressedTokenInstructionDataBurn,
121    authority: &Pubkey,
122    remaining_accounts: &[AccountInfo<'_>],
123    mint: &Pubkey,
124) -> Result<(
125    Vec<InAccount>,
126    Vec<OutputCompressedAccountWithPackedContext>,
127)> {
128    let (mut compressed_input_accounts, input_token_data, sum_lamports) =
129        get_input_compressed_accounts_with_merkle_context_and_check_signer::<NOT_FROZEN>(
130            authority,
131            &inputs.delegated_transfer,
132            remaining_accounts,
133            &inputs.input_token_data_with_context,
134            mint,
135        )?;
136    let sum_inputs = input_token_data.iter().map(|x| x.amount).sum::<u64>();
137    let change_amount = match sum_inputs.checked_sub(inputs.burn_amount) {
138        Some(change_amount) => change_amount,
139        None => return err!(ErrorCode::ArithmeticUnderflow),
140    };
141
142    let hashed_mint = hash_to_bn254_field_size_be(&mint.to_bytes());
143    let output_compressed_accounts = if change_amount > 0 || sum_lamports > 0 {
144        let (is_delegate, authority, delegate) =
145            if let Some(delegated_transfer) = inputs.delegated_transfer.as_ref() {
146                let mut vec = vec![false; 1];
147                if let Some(index) = delegated_transfer.delegate_change_account_index {
148                    vec[index as usize] = true;
149                } else {
150                    return err!(crate::ErrorCode::InvalidDelegateIndex);
151                }
152                (Some(vec), delegated_transfer.owner, Some(*authority))
153            } else {
154                (None, *authority, None)
155            };
156        let mut output_compressed_accounts =
157            vec![OutputCompressedAccountWithPackedContext::default(); 1];
158        let lamports = if sum_lamports > 0 {
159            Some(vec![Some(sum_lamports)])
160        } else {
161            None
162        };
163        create_output_compressed_accounts(
164            &mut output_compressed_accounts,
165            *mint,
166            &[authority; 1],
167            delegate,
168            is_delegate,
169            &[change_amount],
170            lamports,
171            &hashed_mint,
172            &[inputs.change_account_merkle_tree_index],
173            remaining_accounts,
174        )?;
175        output_compressed_accounts
176    } else {
177        Vec::new()
178    };
179    add_data_hash_to_input_compressed_accounts::<NOT_FROZEN>(
180        &mut compressed_input_accounts,
181        input_token_data.as_slice(),
182        &hashed_mint,
183        remaining_accounts,
184    )?;
185    Ok((compressed_input_accounts, output_compressed_accounts))
186}
187
188#[cfg(not(target_os = "solana"))]
189pub mod sdk {
190
191    use anchor_lang::{AnchorSerialize, InstructionData, ToAccountMetas};
192    use light_compressed_account::{
193        compressed_account::{CompressedAccount, MerkleContext},
194        instruction_data::compressed_proof::CompressedProof,
195    };
196    use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
197
198    use super::CompressedTokenInstructionDataBurn;
199    use crate::{
200        get_token_pool_pda_with_index,
201        process_transfer::{
202            get_cpi_authority_pda,
203            transfer_sdk::{
204                create_input_output_and_remaining_accounts, to_account_metas, TransferSdkError,
205            },
206            DelegatedTransfer,
207        },
208        token_data::TokenData,
209    };
210
211    pub struct CreateBurnInstructionInputs {
212        pub fee_payer: Pubkey,
213        pub authority: Pubkey,
214        pub root_indices: Vec<Option<u16>>,
215        pub proof: CompressedProof,
216        pub input_token_data: Vec<TokenData>,
217        pub input_compressed_accounts: Vec<CompressedAccount>,
218        pub input_merkle_contexts: Vec<MerkleContext>,
219        pub change_account_merkle_tree: Pubkey,
220        pub mint: Pubkey,
221        pub burn_amount: u64,
222        pub signer_is_delegate: bool,
223        pub is_token_22: bool,
224        pub token_pool_index: u8,
225        pub additional_pool_accounts: Vec<Pubkey>,
226    }
227
228    pub fn create_burn_instruction(
229        inputs: CreateBurnInstructionInputs,
230    ) -> Result<Instruction, TransferSdkError> {
231        let (remaining_accounts, input_token_data_with_context, _) =
232            create_input_output_and_remaining_accounts(
233                &[
234                    inputs.additional_pool_accounts,
235                    vec![inputs.change_account_merkle_tree],
236                ]
237                .concat(),
238                &inputs.input_token_data,
239                &inputs.input_compressed_accounts,
240                &inputs.input_merkle_contexts,
241                &inputs.root_indices,
242                &Vec::new(),
243            );
244        let outputs_merkle_tree_index =
245            match remaining_accounts.get(&inputs.change_account_merkle_tree) {
246                Some(index) => index,
247                None => return Err(TransferSdkError::AccountNotFound),
248            };
249        let delegated_transfer = if inputs.signer_is_delegate {
250            let delegated_transfer = DelegatedTransfer {
251                owner: inputs.input_token_data[0].owner,
252                delegate_change_account_index: Some(0),
253            };
254            Some(delegated_transfer)
255        } else {
256            None
257        };
258        let inputs_struct = CompressedTokenInstructionDataBurn {
259            proof: inputs.proof,
260            input_token_data_with_context,
261            cpi_context: None,
262            change_account_merkle_tree_index: *outputs_merkle_tree_index as u8,
263            delegated_transfer,
264            burn_amount: inputs.burn_amount,
265        };
266        let remaining_accounts = to_account_metas(remaining_accounts);
267        let mut serialized_ix_data = Vec::new();
268        CompressedTokenInstructionDataBurn::serialize(&inputs_struct, &mut serialized_ix_data)
269            .map_err(|_| TransferSdkError::SerializationError)?;
270
271        let (cpi_authority_pda, _) = get_cpi_authority_pda();
272        let data = crate::instruction::Burn {
273            inputs: serialized_ix_data,
274        }
275        .data();
276
277        let token_pool_pda = get_token_pool_pda_with_index(&inputs.mint, inputs.token_pool_index);
278        let token_program = if inputs.is_token_22 {
279            anchor_spl::token_2022::ID
280        } else {
281            spl_token::ID
282        };
283        let accounts = crate::accounts::BurnInstruction {
284            fee_payer: inputs.fee_payer,
285            authority: inputs.authority,
286            cpi_authority_pda,
287            mint: inputs.mint,
288            token_pool_pda,
289            token_program,
290            light_system_program: light_system_program::ID,
291            registered_program_pda: light_system_program::utils::get_registered_program_pda(
292                &light_system_program::ID,
293            ),
294            noop_program: Pubkey::new_from_array(
295                account_compression::utils::constants::NOOP_PUBKEY,
296            ),
297            account_compression_authority: light_system_program::utils::get_cpi_authority_pda(
298                &light_system_program::ID,
299            ),
300            account_compression_program: account_compression::ID,
301            self_program: crate::ID,
302            system_program: solana_sdk::system_program::ID,
303        };
304
305        Ok(Instruction {
306            program_id: crate::ID,
307            accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(),
308
309            data,
310        })
311    }
312}
313
314#[cfg(test)]
315mod test {
316
317    use account_compression::StateMerkleTreeAccount;
318    use anchor_lang::{solana_program::account_info::AccountInfo, Discriminator};
319    use light_compressed_account::compressed_account::PackedMerkleContext;
320    use rand::Rng;
321
322    use super::*;
323    use crate::{
324        freeze::test_freeze::{
325            create_expected_input_accounts, create_expected_token_output_accounts,
326            get_rnd_input_token_data_with_contexts,
327        },
328        token_data::AccountState,
329        TokenData,
330    };
331
332    // TODO: add randomized and edge case tests
333    #[test]
334    fn test_burn() {
335        let merkle_tree_pubkey = Pubkey::new_unique();
336        let mut merkle_tree_account_lamports = 0;
337        let mut merkle_tree_account_data = StateMerkleTreeAccount::DISCRIMINATOR.to_vec();
338        let nullifier_queue_pubkey = Pubkey::new_unique();
339        let mut nullifier_queue_account_lamports = 0;
340        let mut nullifier_queue_account_data = Vec::new();
341        let merkle_tree_pubkey_1 = Pubkey::new_unique();
342        let mut merkle_tree_account_lamports_1 = 0;
343        let mut merkle_tree_account_data_1 = StateMerkleTreeAccount::DISCRIMINATOR.to_vec();
344        let remaining_accounts = vec![
345            AccountInfo::new(
346                &merkle_tree_pubkey,
347                false,
348                false,
349                &mut merkle_tree_account_lamports,
350                &mut merkle_tree_account_data,
351                &account_compression::ID,
352                false,
353                0,
354            ),
355            AccountInfo::new(
356                &nullifier_queue_pubkey,
357                false,
358                false,
359                &mut nullifier_queue_account_lamports,
360                &mut nullifier_queue_account_data,
361                &account_compression::ID,
362                false,
363                0,
364            ),
365            AccountInfo::new(
366                &merkle_tree_pubkey_1,
367                false,
368                false,
369                &mut merkle_tree_account_lamports_1,
370                &mut merkle_tree_account_data_1,
371                &account_compression::ID,
372                false,
373                0,
374            ),
375        ];
376        let authority = Pubkey::new_unique();
377        let mint = Pubkey::new_unique();
378        let test_amounts = vec![0, 1, 10, 100, 1_000, 10_000, 100_000, 1_000_000];
379        for test_amount in test_amounts {
380            let input_token_data_with_context = vec![InputTokenDataWithContext {
381                amount: test_amount,
382                merkle_context: PackedMerkleContext {
383                    merkle_tree_pubkey_index: 0,
384                    queue_pubkey_index: 1,
385                    leaf_index: 1,
386                    prove_by_index: false,
387                },
388                root_index: 0,
389                delegate_index: Some(1),
390                lamports: None,
391                tlv: None,
392            }];
393            let inputs = CompressedTokenInstructionDataBurn {
394                proof: CompressedProof::default(),
395                input_token_data_with_context,
396                cpi_context: None,
397                burn_amount: std::cmp::min(50, test_amount),
398                change_account_merkle_tree_index: 2,
399                delegated_transfer: None,
400            };
401            let (compressed_input_accounts, output_compressed_accounts) =
402                create_input_and_output_accounts_burn(
403                    &inputs,
404                    &authority,
405                    &remaining_accounts,
406                    &mint,
407                )
408                .unwrap();
409            assert_eq!(compressed_input_accounts.len(), 1);
410            let change_amount = test_amount.saturating_sub(inputs.burn_amount);
411            assert_eq!(
412                output_compressed_accounts.len(),
413                std::cmp::min(1, change_amount) as usize
414            );
415            if change_amount != 0 {
416                let expected_change_token_data = TokenData {
417                    mint,
418                    owner: authority,
419                    amount: change_amount,
420                    delegate: None,
421                    state: AccountState::Initialized,
422                    tlv: None,
423                };
424                let expected_compressed_output_accounts = create_expected_token_output_accounts(
425                    vec![expected_change_token_data],
426                    vec![2],
427                );
428
429                assert_eq!(
430                    output_compressed_accounts,
431                    expected_compressed_output_accounts
432                );
433            }
434        }
435    }
436
437    #[test]
438    fn test_rnd_burn() {
439        let mut rng = rand::rngs::ThreadRng::default();
440        let merkle_tree_pubkey = Pubkey::new_unique();
441        let mut merkle_tree_account_lamports = 0;
442        let mut merkle_tree_account_data = StateMerkleTreeAccount::DISCRIMINATOR.to_vec();
443        let nullifier_queue_pubkey = Pubkey::new_unique();
444        let mut nullifier_queue_account_lamports = 0;
445        let mut nullifier_queue_account_data = Vec::new();
446        let merkle_tree_pubkey_1 = Pubkey::new_unique();
447        let mut merkle_tree_account_lamports_1 = 0;
448        let mut merkle_tree_account_data_1 = StateMerkleTreeAccount::DISCRIMINATOR.to_vec();
449        let remaining_accounts = vec![
450            AccountInfo::new(
451                &merkle_tree_pubkey,
452                false,
453                false,
454                &mut merkle_tree_account_lamports,
455                &mut merkle_tree_account_data,
456                &account_compression::ID,
457                false,
458                0,
459            ),
460            AccountInfo::new(
461                &nullifier_queue_pubkey,
462                false,
463                false,
464                &mut nullifier_queue_account_lamports,
465                &mut nullifier_queue_account_data,
466                &account_compression::ID,
467                false,
468                0,
469            ),
470            AccountInfo::new(
471                &merkle_tree_pubkey_1,
472                false,
473                false,
474                &mut merkle_tree_account_lamports_1,
475                &mut merkle_tree_account_data_1,
476                &account_compression::ID,
477                false,
478                0,
479            ),
480        ];
481
482        let iter = 1_000;
483        for _ in 0..iter {
484            let authority = Pubkey::new_unique();
485            let mint = Pubkey::new_unique();
486            let num_inputs = rng.gen_range(1..=8);
487            let input_token_data_with_context =
488                get_rnd_input_token_data_with_contexts(&mut rng, num_inputs);
489            let sum_inputs = input_token_data_with_context
490                .iter()
491                .map(|x| x.amount)
492                .sum::<u64>();
493            let burn_amount = rng.gen_range(0..sum_inputs);
494            let inputs = CompressedTokenInstructionDataBurn {
495                proof: CompressedProof::default(),
496                input_token_data_with_context: input_token_data_with_context.clone(),
497                cpi_context: None,
498                burn_amount,
499                change_account_merkle_tree_index: 2,
500                delegated_transfer: None,
501            };
502            let (compressed_input_accounts, output_compressed_accounts) =
503                create_input_and_output_accounts_burn(
504                    &inputs,
505                    &authority,
506                    &remaining_accounts,
507                    &mint,
508                )
509                .unwrap();
510            let expected_input_accounts = create_expected_input_accounts(
511                &input_token_data_with_context,
512                &mint,
513                &authority,
514                remaining_accounts
515                    .iter()
516                    .map(|x| x.key)
517                    .cloned()
518                    .collect::<Vec<_>>()
519                    .as_slice(),
520            );
521            assert_eq!(compressed_input_accounts, expected_input_accounts);
522            assert_eq!(compressed_input_accounts.len(), num_inputs);
523            assert_eq!(output_compressed_accounts.len(), 1);
524            let expected_change_token_data = TokenData {
525                mint,
526                owner: authority,
527                amount: sum_inputs - burn_amount,
528                delegate: None,
529                state: AccountState::Initialized,
530                tlv: None,
531            };
532            let expected_compressed_output_accounts =
533                create_expected_token_output_accounts(vec![expected_change_token_data], vec![2]);
534
535            assert_eq!(
536                output_compressed_accounts,
537                expected_compressed_output_accounts
538            );
539        }
540    }
541
542    #[test]
543    fn failing_tests_burn() {
544        let merkle_tree_pubkey = Pubkey::new_unique();
545        let mut merkle_tree_account_lamports = 0;
546        let mut merkle_tree_account_data = StateMerkleTreeAccount::DISCRIMINATOR.to_vec();
547        let nullifier_queue_pubkey = Pubkey::new_unique();
548        let mut nullifier_queue_account_lamports = 0;
549        let mut nullifier_queue_account_data = Vec::new();
550        let merkle_tree_pubkey_1 = Pubkey::new_unique();
551        let mut merkle_tree_account_lamports_1 = 0;
552        let mut merkle_tree_account_data_1 = StateMerkleTreeAccount::DISCRIMINATOR.to_vec();
553        let remaining_accounts = vec![
554            AccountInfo::new(
555                &merkle_tree_pubkey,
556                false,
557                false,
558                &mut merkle_tree_account_lamports,
559                &mut merkle_tree_account_data,
560                &account_compression::ID,
561                false,
562                0,
563            ),
564            AccountInfo::new(
565                &nullifier_queue_pubkey,
566                false,
567                false,
568                &mut nullifier_queue_account_lamports,
569                &mut nullifier_queue_account_data,
570                &account_compression::ID,
571                false,
572                0,
573            ),
574            AccountInfo::new(
575                &merkle_tree_pubkey_1,
576                false,
577                false,
578                &mut merkle_tree_account_lamports_1,
579                &mut merkle_tree_account_data_1,
580                &account_compression::ID,
581                false,
582                0,
583            ),
584        ];
585        let authority = Pubkey::new_unique();
586        let mint = Pubkey::new_unique();
587        let input_token_data_with_context = vec![InputTokenDataWithContext {
588            amount: 100,
589            merkle_context: PackedMerkleContext {
590                merkle_tree_pubkey_index: 0,
591                queue_pubkey_index: 1,
592                leaf_index: 1,
593                prove_by_index: false,
594            },
595            root_index: 0,
596            delegate_index: Some(1),
597            lamports: None,
598            tlv: None,
599        }];
600
601        // Burn amount too high
602        {
603            let mut invalid_input_token_data_with_context = input_token_data_with_context.clone();
604            invalid_input_token_data_with_context[0].amount = 0;
605            let inputs = CompressedTokenInstructionDataBurn {
606                proof: CompressedProof::default(),
607                input_token_data_with_context: invalid_input_token_data_with_context,
608                cpi_context: None,
609                burn_amount: 50,
610                change_account_merkle_tree_index: 2,
611                delegated_transfer: None,
612            };
613            let result = create_input_and_output_accounts_burn(
614                &inputs,
615                &authority,
616                &remaining_accounts,
617                &mint,
618            );
619            let error_code = ErrorCode::ArithmeticUnderflow as u32 + 6000;
620            assert!(matches!(
621                result.unwrap_err(),
622                anchor_lang::error::Error::AnchorError(error) if error.error_code_number == error_code
623            ));
624        }
625        // invalid authority
626        {
627            let invalid_authority = Pubkey::new_unique();
628            let inputs = CompressedTokenInstructionDataBurn {
629                proof: CompressedProof::default(),
630                input_token_data_with_context: input_token_data_with_context.clone(),
631                cpi_context: None,
632                burn_amount: 50,
633                change_account_merkle_tree_index: 2,
634                delegated_transfer: None,
635            };
636            let (compressed_input_accounts, output_compressed_accounts) =
637                create_input_and_output_accounts_burn(
638                    &inputs,
639                    &invalid_authority,
640                    &remaining_accounts,
641                    &mint,
642                )
643                .unwrap();
644            let expected_input_accounts = create_expected_input_accounts(
645                &input_token_data_with_context,
646                &mint,
647                &invalid_authority,
648                remaining_accounts
649                    .iter()
650                    .map(|x| x.key)
651                    .cloned()
652                    .collect::<Vec<_>>()
653                    .as_slice(),
654            );
655            assert_eq!(compressed_input_accounts, expected_input_accounts);
656            let expected_change_token_data = TokenData {
657                mint,
658                owner: invalid_authority,
659                amount: 50,
660                delegate: None,
661                state: AccountState::Initialized,
662                tlv: None,
663            };
664            let expected_compressed_output_accounts =
665                create_expected_token_output_accounts(vec![expected_change_token_data], vec![2]);
666
667            assert_eq!(
668                output_compressed_accounts,
669                expected_compressed_output_accounts
670            );
671        }
672        // Invalid Mint
673        {
674            let mut invalid_input_token_data_with_context = input_token_data_with_context.clone();
675            invalid_input_token_data_with_context[0].amount = 0;
676            let invalid_mint = Pubkey::new_unique();
677            let inputs = CompressedTokenInstructionDataBurn {
678                proof: CompressedProof::default(),
679                input_token_data_with_context: input_token_data_with_context.clone(),
680                cpi_context: None,
681                burn_amount: 50,
682                change_account_merkle_tree_index: 2,
683                delegated_transfer: None,
684            };
685            let (compressed_input_accounts, output_compressed_accounts) =
686                create_input_and_output_accounts_burn(
687                    &inputs,
688                    &authority,
689                    &remaining_accounts,
690                    &invalid_mint,
691                )
692                .unwrap();
693            assert_eq!(compressed_input_accounts.len(), 1);
694            assert_eq!(output_compressed_accounts.len(), 1);
695            let expected_input_accounts = create_expected_input_accounts(
696                &input_token_data_with_context,
697                &invalid_mint,
698                &authority,
699                remaining_accounts
700                    .iter()
701                    .map(|x| x.key)
702                    .cloned()
703                    .collect::<Vec<_>>()
704                    .as_slice(),
705            );
706            assert_eq!(compressed_input_accounts, expected_input_accounts);
707            let expected_change_token_data = TokenData {
708                mint: invalid_mint,
709                owner: authority,
710                amount: 50,
711                delegate: None,
712                state: AccountState::Initialized,
713                tlv: None,
714            };
715            let expected_compressed_output_accounts =
716                create_expected_token_output_accounts(vec![expected_change_token_data], vec![2]);
717
718            assert_eq!(
719                output_compressed_accounts,
720                expected_compressed_output_accounts
721            );
722        }
723    }
724}