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