1use ::spl_token_interface::state::Account as SplTokenAccount;
11use solana_sdk::{
12 account::Account as SolanaAccount, instruction::Instruction, program_pack::Pack, pubkey::Pubkey,
13};
14use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id;
15use tracing::error;
16
17use spl_associated_token_account_interface::instruction::create_associated_token_account;
18
19use crate::services::provider::SolanaProviderTrait;
20
21#[derive(Debug, Clone, Copy)]
26pub struct TokenAccount {
27 pub mint: Pubkey,
29 pub owner: Pubkey,
31 pub amount: u64,
33 pub is_frozen: bool,
35}
36
37#[derive(Debug, thiserror::Error)]
42pub enum TokenError {
43 #[error("Invalid token instruction: {0}")]
45 InvalidTokenInstruction(String),
46 #[error("Invalid token mint: {0}")]
48 InvalidTokenMint(String),
49 #[error("Invalid token program: {0}")]
51 InvalidTokenProgram(String),
52 #[error("Instruction error: {0}")]
54 Instruction(String),
55 #[error("Account error: {0}")]
57 AccountError(String),
58}
59
60#[derive(Debug)]
65pub enum TokenInstruction {
66 Transfer { amount: u64 },
68 TransferChecked { amount: u64, decimals: u8 },
70 Other,
72}
73
74pub struct SolanaTokenProgram;
79
80impl SolanaTokenProgram {
81 pub async fn get_token_program_for_mint<P: SolanaProviderTrait>(
83 provider: &P,
84 mint: &Pubkey,
85 ) -> Result<Pubkey, TokenError> {
86 let account = provider
87 .get_account_from_pubkey(mint)
88 .await
89 .map_err(|e| TokenError::InvalidTokenMint(e.to_string()))?;
90
91 if account.owner == spl_token_interface::id() {
92 Ok(spl_token_interface::id())
93 } else if account.owner == spl_token_2022_interface::id() {
94 Ok(spl_token_2022_interface::id())
95 } else {
96 Err(TokenError::InvalidTokenProgram(format!(
97 "Unknown token program: {}",
98 account.owner
99 )))
100 }
101 }
102
103 pub fn is_token_program(program_id: &Pubkey) -> bool {
113 program_id == &spl_token_interface::id() || program_id == &spl_token_2022_interface::id()
114 }
115
116 pub fn create_transfer_checked_instruction(
132 program_id: &Pubkey,
133 source: &Pubkey,
134 mint: &Pubkey,
135 destination: &Pubkey,
136 authority: &Pubkey,
137 amount: u64,
138 decimals: u8,
139 ) -> Result<Instruction, TokenError> {
140 if !Self::is_token_program(program_id) {
141 return Err(TokenError::InvalidTokenProgram(format!(
142 "Unknown token program: {program_id}"
143 )));
144 }
145 if program_id == &spl_token_interface::id() {
146 return spl_token_interface::instruction::transfer_checked(
147 program_id,
148 source,
149 mint,
150 destination,
151 authority,
152 &[],
153 amount,
154 decimals,
155 )
156 .map_err(|e| TokenError::Instruction(e.to_string()));
157 } else if program_id == &spl_token_2022_interface::id() {
158 return spl_token_2022_interface::instruction::transfer_checked(
159 program_id,
160 source,
161 mint,
162 destination,
163 authority,
164 &[],
165 amount,
166 decimals,
167 )
168 .map_err(|e| TokenError::Instruction(e.to_string()));
169 }
170 Err(TokenError::InvalidTokenProgram(format!(
171 "Unknown token program: {program_id}"
172 )))
173 }
174
175 pub fn unpack_account(
186 program_id: &Pubkey,
187 account: &SolanaAccount,
188 ) -> Result<TokenAccount, TokenError> {
189 if !Self::is_token_program(program_id) {
190 return Err(TokenError::InvalidTokenProgram(format!(
191 "Unknown token program: {program_id}"
192 )));
193 }
194 if program_id == &spl_token_interface::id() {
195 let account = SplTokenAccount::unpack(&account.data)
196 .map_err(|e| TokenError::AccountError(format!("Invalid token account1: {e}")))?;
197
198 return Ok(TokenAccount {
199 mint: account.mint,
200 owner: account.owner,
201 amount: account.amount,
202 is_frozen: account.is_frozen(),
203 });
204 } else if program_id == &spl_token_2022_interface::id() {
205 let state_with_extensions = spl_token_2022_interface::extension::StateWithExtensions::<
206 spl_token_2022_interface::state::Account,
207 >::unpack(&account.data)
208 .map_err(|e| TokenError::AccountError(format!("Invalid token account2: {e}")))?;
209
210 let base_account = state_with_extensions.base;
211
212 return Ok(TokenAccount {
213 mint: base_account.mint,
214 owner: base_account.owner,
215 amount: base_account.amount,
216 is_frozen: base_account.is_frozen(),
217 });
218 }
219 Err(TokenError::InvalidTokenProgram(format!(
220 "Unknown token program: {program_id}"
221 )))
222 }
223
224 pub fn get_associated_token_address(
236 program_id: &Pubkey,
237 wallet: &Pubkey,
238 mint: &Pubkey,
239 ) -> Pubkey {
240 get_associated_token_address_with_program_id(wallet, mint, program_id)
241 }
242
243 pub fn create_associated_token_account(
256 program_id: &Pubkey,
257 payer: &Pubkey,
258 wallet: &Pubkey,
259 mint: &Pubkey,
260 ) -> Instruction {
261 create_associated_token_account(payer, wallet, mint, program_id)
262 }
263
264 pub fn unpack_instruction(
275 program_id: &Pubkey,
276 data: &[u8],
277 ) -> Result<TokenInstruction, TokenError> {
278 if !Self::is_token_program(program_id) {
279 return Err(TokenError::InvalidTokenProgram(format!(
280 "Unknown token program: {program_id}"
281 )));
282 }
283 if program_id == &spl_token_interface::id() {
284 match spl_token_interface::instruction::TokenInstruction::unpack(data) {
285 Ok(instr) => match instr {
286 spl_token_interface::instruction::TokenInstruction::Transfer { amount } => {
287 Ok(TokenInstruction::Transfer { amount })
288 }
289 spl_token_interface::instruction::TokenInstruction::TransferChecked {
290 amount,
291 decimals,
292 } => Ok(TokenInstruction::TransferChecked { amount, decimals }),
293 _ => Ok(TokenInstruction::Other), },
295 Err(e) => Err(TokenError::InvalidTokenInstruction(e.to_string())),
296 }
297 } else if program_id == &spl_token_2022_interface::id() {
298 match spl_token_2022_interface::instruction::TokenInstruction::unpack(data) {
299 Ok(instr) => match instr {
300 #[allow(deprecated)]
301 spl_token_2022_interface::instruction::TokenInstruction::Transfer {
302 amount,
303 } => Ok(TokenInstruction::Transfer { amount }),
304 spl_token_2022_interface::instruction::TokenInstruction::TransferChecked {
305 amount,
306 decimals,
307 } => Ok(TokenInstruction::TransferChecked { amount, decimals }),
308 _ => Ok(TokenInstruction::Other), },
310 Err(e) => Err(TokenError::InvalidTokenInstruction(e.to_string())),
311 }
312 } else {
313 Err(TokenError::InvalidTokenProgram(format!(
314 "Unknown token program: {program_id}"
315 )))
316 }
317 }
318
319 pub async fn get_and_unpack_token_account<P: SolanaProviderTrait>(
337 provider: &P,
338 owner: &Pubkey,
339 mint: &Pubkey,
340 ) -> Result<TokenAccount, TokenError> {
341 let program_id = Self::get_token_program_for_mint(provider, mint).await?;
343
344 let token_account_address = Self::get_associated_token_address(&program_id, owner, mint);
346
347 let account_data = provider
349 .get_account_from_pubkey(&token_account_address)
350 .await
351 .map_err(|e| {
352 TokenError::AccountError(format!(
353 "Failed to fetch token account for owner {owner} and mint {mint}: {e}"
354 ))
355 })?;
356
357 Self::unpack_account(&program_id, &account_data)
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use mockall::predicate::eq;
365 use solana_sdk::{program_pack::Pack, pubkey::Pubkey};
366 use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id;
367 use spl_associated_token_account_interface::instruction::create_associated_token_account;
368 use spl_token_interface::state::Account;
369
370 use crate::{
371 domain::{SolanaTokenProgram, TokenError, TokenInstruction},
372 services::provider::MockSolanaProviderTrait,
373 };
374
375 #[tokio::test]
376 async fn test_get_token_program_for_mint_spl_token() {
377 let mint = Pubkey::new_unique();
378 let mut mock_provider = MockSolanaProviderTrait::new();
379
380 mock_provider
381 .expect_get_account_from_pubkey()
382 .with(eq(mint))
383 .times(1)
384 .returning(|_| {
385 Box::pin(async {
386 Ok(solana_sdk::account::Account {
387 lamports: 1000000,
388 data: vec![],
389 owner: spl_token_interface::id(),
390 executable: false,
391 rent_epoch: 0,
392 })
393 })
394 });
395
396 let result = SolanaTokenProgram::get_token_program_for_mint(&mock_provider, &mint).await;
397
398 assert!(result.is_ok());
399 assert_eq!(result.unwrap(), spl_token_interface::id());
400 }
401
402 #[tokio::test]
403 async fn test_get_token_program_for_mint_token_2022() {
404 let mint = Pubkey::new_unique();
405 let mut mock_provider = MockSolanaProviderTrait::new();
406
407 mock_provider
408 .expect_get_account_from_pubkey()
409 .with(eq(mint))
410 .times(1)
411 .returning(|_| {
412 Box::pin(async {
413 Ok(solana_sdk::account::Account {
414 lamports: 1000000,
415 data: vec![],
416 owner: spl_token_2022_interface::id(),
417 executable: false,
418 rent_epoch: 0,
419 })
420 })
421 });
422
423 let result = SolanaTokenProgram::get_token_program_for_mint(&mock_provider, &mint).await;
424 assert!(result.is_ok());
425 assert_eq!(result.unwrap(), spl_token_2022_interface::id());
426 }
427
428 #[tokio::test]
429 async fn test_get_token_program_for_mint_invalid() {
430 let mint = Pubkey::new_unique();
431 let mut mock_provider = MockSolanaProviderTrait::new();
432
433 mock_provider
434 .expect_get_account_from_pubkey()
435 .with(eq(mint))
436 .times(1)
437 .returning(|_| {
438 Box::pin(async {
439 Ok(solana_sdk::account::Account {
440 lamports: 1000000,
441 data: vec![],
442 owner: Pubkey::new_unique(),
443 executable: false,
444 rent_epoch: 0,
445 })
446 })
447 });
448
449 let result = SolanaTokenProgram::get_token_program_for_mint(&mock_provider, &mint).await;
450 assert!(result.is_err());
451 assert!(matches!(
452 result.unwrap_err(),
453 TokenError::InvalidTokenProgram(_)
454 ));
455 }
456
457 #[test]
458 fn test_is_token_program() {
459 assert!(SolanaTokenProgram::is_token_program(
460 &spl_token_interface::id()
461 ));
462 assert!(SolanaTokenProgram::is_token_program(
463 &spl_token_2022_interface::id()
464 ));
465 assert!(!SolanaTokenProgram::is_token_program(&Pubkey::new_unique()));
466 }
467
468 #[test]
469 fn test_create_transfer_checked_instruction_spl_token() {
470 let program_id = spl_token_interface::id();
471 let source = Pubkey::new_unique();
472 let mint = Pubkey::new_unique();
473 let destination = Pubkey::new_unique();
474 let authority = Pubkey::new_unique();
475 let amount = 1000;
476 let decimals = 9;
477
478 let result = SolanaTokenProgram::create_transfer_checked_instruction(
479 &program_id,
480 &source,
481 &mint,
482 &destination,
483 &authority,
484 amount,
485 decimals,
486 );
487
488 assert!(result.is_ok());
489 let instruction = result.unwrap();
490 assert_eq!(instruction.program_id, program_id);
491 assert_eq!(instruction.accounts.len(), 4);
492 assert_eq!(instruction.accounts[0].pubkey, source);
493 assert_eq!(instruction.accounts[1].pubkey, mint);
494 assert_eq!(instruction.accounts[2].pubkey, destination);
495 assert_eq!(instruction.accounts[3].pubkey, authority);
496 }
497
498 #[test]
499 fn test_create_transfer_checked_instruction_token_2022() {
500 let program_id = spl_token_2022_interface::id();
501 let source = Pubkey::new_unique();
502 let mint = Pubkey::new_unique();
503 let destination = Pubkey::new_unique();
504 let authority = Pubkey::new_unique();
505 let amount = 1000;
506 let decimals = 9;
507
508 let result = SolanaTokenProgram::create_transfer_checked_instruction(
509 &program_id,
510 &source,
511 &mint,
512 &destination,
513 &authority,
514 amount,
515 decimals,
516 );
517
518 assert!(result.is_ok());
519 let instruction = result.unwrap();
520 assert_eq!(instruction.program_id, program_id);
521 assert_eq!(instruction.accounts.len(), 4);
522 assert_eq!(instruction.accounts[0].pubkey, source);
523 assert_eq!(instruction.accounts[1].pubkey, mint);
524 assert_eq!(instruction.accounts[2].pubkey, destination);
525 assert_eq!(instruction.accounts[3].pubkey, authority);
526 }
527
528 #[test]
529 fn test_create_transfer_checked_instruction_invalid_program() {
530 let program_id = Pubkey::new_unique(); let source = Pubkey::new_unique();
532 let mint = Pubkey::new_unique();
533 let destination = Pubkey::new_unique();
534 let authority = Pubkey::new_unique();
535 let amount = 1000;
536 let decimals = 9;
537
538 let result = SolanaTokenProgram::create_transfer_checked_instruction(
539 &program_id,
540 &source,
541 &mint,
542 &destination,
543 &authority,
544 amount,
545 decimals,
546 );
547
548 assert!(result.is_err());
549 assert!(matches!(
550 result.unwrap_err(),
551 TokenError::InvalidTokenProgram(_)
552 ));
553 }
554
555 #[test]
556 fn test_unpack_account_spl_token() {
557 let program_id = spl_token_interface::id();
558 let mint = Pubkey::new_unique();
559 let owner = Pubkey::new_unique();
560 let amount = 1000;
561
562 let spl_account = Account {
563 mint,
564 owner,
565 amount,
566 state: spl_token_interface::state::AccountState::Initialized,
567 ..Default::default()
568 };
569
570 let mut account_data = vec![0; Account::LEN];
571 Account::pack(spl_account, &mut account_data).unwrap();
572
573 let solana_account = solana_sdk::account::Account {
574 lamports: 0,
575 data: account_data,
576 owner: program_id,
577 executable: false,
578 rent_epoch: 0,
579 };
580
581 let result = SolanaTokenProgram::unpack_account(&program_id, &solana_account);
582 assert!(result.is_ok());
583
584 let token_account = result.unwrap();
585 assert_eq!(token_account.mint, mint);
586 assert_eq!(token_account.owner, owner);
587 assert_eq!(token_account.amount, amount);
588 assert!(!token_account.is_frozen);
589 }
590
591 #[test]
592 fn test_unpack_account_token_2022() {
593 let program_id = spl_token_2022_interface::id();
594 let mint = Pubkey::new_unique();
595 let owner = Pubkey::new_unique();
596 let amount = 1000;
597
598 let spl_account = Account {
599 mint,
600 owner,
601 amount,
602 state: spl_token_interface::state::AccountState::Initialized,
603 ..Default::default()
604 };
605
606 let mut account_data = vec![0; Account::LEN];
607 Account::pack(spl_account, &mut account_data).unwrap();
608
609 let solana_account = solana_sdk::account::Account {
610 lamports: 0,
611 data: account_data,
612 owner: program_id,
613 executable: false,
614 rent_epoch: 0,
615 };
616
617 let result = SolanaTokenProgram::unpack_account(&program_id, &solana_account);
618 assert!(result.is_ok());
619
620 let token_account = result.unwrap();
621 assert_eq!(token_account.mint, mint);
622 assert_eq!(token_account.owner, owner);
623 assert_eq!(token_account.amount, amount);
624 assert!(!token_account.is_frozen);
625 }
626
627 #[test]
628 fn test_unpack_account_invalid_program() {
629 let program_id = Pubkey::new_unique(); let mint = Pubkey::new_unique();
631 let owner = Pubkey::new_unique();
632 let amount = 1000;
633
634 let spl_account = Account {
635 mint,
636 owner,
637 amount,
638 state: spl_token_interface::state::AccountState::Initialized,
639 ..Default::default()
640 };
641
642 let mut account_data = vec![0; Account::LEN];
643 Account::pack(spl_account, &mut account_data).unwrap();
644
645 let account = solana_sdk::account::Account {
646 lamports: 0,
647 data: account_data,
648 owner: program_id,
649 executable: false,
650 rent_epoch: 0,
651 };
652
653 let result = SolanaTokenProgram::unpack_account(&program_id, &account);
654 assert!(result.is_err());
655 assert!(matches!(
656 result.unwrap_err(),
657 TokenError::InvalidTokenProgram(_)
658 ));
659 }
660
661 #[test]
662 fn test_get_associated_token_address_spl_token() {
663 let program_id = spl_token_interface::id();
664 let wallet = Pubkey::new_unique();
665 let mint = Pubkey::new_unique();
666
667 let result = SolanaTokenProgram::get_associated_token_address(&program_id, &wallet, &mint);
668 let expected = get_associated_token_address_with_program_id(&wallet, &mint, &program_id);
669
670 assert_eq!(result, expected);
671 }
672
673 #[test]
674 fn test_get_associated_token_address_token_2022() {
675 let program_id = spl_token_2022_interface::id();
676 let wallet = Pubkey::new_unique();
677 let mint = Pubkey::new_unique();
678
679 let result = SolanaTokenProgram::get_associated_token_address(&program_id, &wallet, &mint);
680 let expected = get_associated_token_address_with_program_id(&wallet, &mint, &program_id);
681
682 assert_eq!(result, expected);
683 }
684
685 #[test]
686 fn test_create_associated_token_account() {
687 let program_id = spl_token_interface::id();
688 let payer = Pubkey::new_unique();
689 let wallet = Pubkey::new_unique();
690 let mint = Pubkey::new_unique();
691
692 let instruction = SolanaTokenProgram::create_associated_token_account(
693 &program_id,
694 &payer,
695 &wallet,
696 &mint,
697 );
698
699 let expected = create_associated_token_account(&payer, &wallet, &mint, &program_id);
700
701 assert_eq!(instruction.program_id, expected.program_id);
702 assert_eq!(instruction.accounts.len(), expected.accounts.len());
703
704 for (i, account) in instruction.accounts.iter().enumerate() {
705 assert_eq!(account.pubkey, expected.accounts[i].pubkey);
706 assert_eq!(account.is_signer, expected.accounts[i].is_signer);
707 assert_eq!(account.is_writable, expected.accounts[i].is_writable);
708 }
709 }
710
711 #[test]
712 fn test_unpack_instruction_spl_token_transfer() {
713 let program_id = spl_token_interface::id();
714 let amount = 1000u64;
715
716 let instruction = spl_token_interface::instruction::transfer(
717 &program_id,
718 &Pubkey::new_unique(),
719 &Pubkey::new_unique(),
720 &Pubkey::new_unique(),
721 &[],
722 amount,
723 )
724 .unwrap();
725
726 let result = SolanaTokenProgram::unpack_instruction(&program_id, &instruction.data);
727 assert!(result.is_ok());
728
729 if let TokenInstruction::Transfer {
730 amount: parsed_amount,
731 } = result.unwrap()
732 {
733 assert_eq!(parsed_amount, amount);
734 } else {
735 panic!("Expected Transfer instruction");
736 }
737 }
738
739 #[test]
740 fn test_unpack_instruction_spl_token_transfer_checked() {
741 let program_id = spl_token_interface::id();
742 let amount = 1000u64;
743 let decimals = 9u8;
744
745 let instruction = spl_token_interface::instruction::transfer_checked(
746 &program_id,
747 &Pubkey::new_unique(),
748 &Pubkey::new_unique(),
749 &Pubkey::new_unique(),
750 &Pubkey::new_unique(),
751 &[],
752 amount,
753 decimals,
754 )
755 .unwrap();
756
757 let result = SolanaTokenProgram::unpack_instruction(&program_id, &instruction.data);
758 assert!(result.is_ok());
759
760 if let TokenInstruction::TransferChecked {
761 amount: parsed_amount,
762 decimals: parsed_decimals,
763 } = result.unwrap()
764 {
765 assert_eq!(parsed_amount, amount);
766 assert_eq!(parsed_decimals, decimals);
767 } else {
768 panic!("Expected TransferChecked instruction");
769 }
770 }
771
772 #[test]
773 fn test_unpack_instruction_token_2022_transfer() {
774 let program_id = spl_token_2022_interface::id();
775 let amount = 1000u64;
776
777 #[allow(deprecated)]
778 let instruction = spl_token_2022_interface::instruction::transfer(
779 &program_id,
780 &Pubkey::new_unique(),
781 &Pubkey::new_unique(),
782 &Pubkey::new_unique(),
783 &[],
784 amount,
785 )
786 .unwrap();
787
788 let result = SolanaTokenProgram::unpack_instruction(&program_id, &instruction.data);
789 assert!(result.is_ok());
790
791 if let TokenInstruction::Transfer {
792 amount: parsed_amount,
793 } = result.unwrap()
794 {
795 assert_eq!(parsed_amount, amount);
796 } else {
797 panic!("Expected Transfer instruction");
798 }
799 }
800
801 #[test]
802 fn test_unpack_instruction_token_2022_transfer_checked() {
803 let program_id = spl_token_2022_interface::id();
804 let amount = 1000u64;
805 let decimals = 9u8;
806
807 let instruction = spl_token_2022_interface::instruction::transfer_checked(
808 &program_id,
809 &Pubkey::new_unique(),
810 &Pubkey::new_unique(),
811 &Pubkey::new_unique(),
812 &Pubkey::new_unique(),
813 &[],
814 amount,
815 decimals,
816 )
817 .unwrap();
818
819 let result = SolanaTokenProgram::unpack_instruction(&program_id, &instruction.data);
820 assert!(result.is_ok());
821
822 if let TokenInstruction::TransferChecked {
823 amount: parsed_amount,
824 decimals: parsed_decimals,
825 } = result.unwrap()
826 {
827 assert_eq!(parsed_amount, amount);
828 assert_eq!(parsed_decimals, decimals);
829 } else {
830 panic!("Expected TransferChecked instruction");
831 }
832 }
833
834 #[test]
835 fn test_unpack_instruction_invalid_program() {
836 let program_id = Pubkey::new_unique(); let data = vec![0, 1, 2, 3];
838
839 let result = SolanaTokenProgram::unpack_instruction(&program_id, &data);
840 assert!(result.is_err());
841 assert!(matches!(
842 result.unwrap_err(),
843 TokenError::InvalidTokenProgram(_)
844 ));
845 }
846}