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