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 #[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 {
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 {
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 {
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}