1use jsonrpc_core::{BoxFuture, Result};
2use jsonrpc_derive::rpc;
3use solana_account_decoder::{
4 parse_account_data::SplTokenAdditionalDataV2,
5 parse_token::{parse_token_v3, TokenAccountType, UiTokenAmount},
6 UiAccount,
7};
8use solana_client::{
9 rpc_config::RpcAccountInfoConfig,
10 rpc_response::{RpcBlockCommitment, RpcResponseContext},
11};
12use solana_clock::Slot;
13use solana_commitment_config::CommitmentConfig;
14use solana_rpc_client_api::response::Response as RpcResponse;
15use solana_runtime::commitment::BlockCommitmentArray;
16use solana_sdk::program_pack::Pack;
17use spl_token::state::{Account as TokenAccount, Mint};
18
19use super::{RunloopContext, SurfnetRpcContext};
20use crate::{
21 error::{SurfpoolError, SurfpoolResult},
22 rpc::{utils::verify_pubkey, State},
23 surfnet::locker::SvmAccessContext,
24};
25
26#[rpc]
27pub trait AccountsData {
28 type Metadata;
29
30 #[rpc(meta, name = "getAccountInfo")]
92 fn get_account_info(
93 &self,
94 meta: Self::Metadata,
95 pubkey_str: String,
96 config: Option<RpcAccountInfoConfig>,
97 ) -> BoxFuture<Result<RpcResponse<Option<UiAccount>>>>;
98
99 #[rpc(meta, name = "getBlockCommitment")]
143 fn get_block_commitment(
144 &self,
145 meta: Self::Metadata,
146 block: Slot,
147 ) -> Result<RpcBlockCommitment<BlockCommitmentArray>>;
148
149 #[rpc(meta, name = "getMultipleAccounts")]
217 fn get_multiple_accounts(
218 &self,
219 meta: Self::Metadata,
220 pubkey_strs: Vec<String>,
221 config: Option<RpcAccountInfoConfig>,
222 ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>>;
223
224 #[rpc(meta, name = "getTokenAccountBalance")]
279 fn get_token_account_balance(
280 &self,
281 meta: Self::Metadata,
282 pubkey_str: String,
283 commitment: Option<CommitmentConfig>,
284 ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>>;
285
286 #[rpc(meta, name = "getTokenSupply")]
341 fn get_token_supply(
342 &self,
343 meta: Self::Metadata,
344 mint_str: String,
345 commitment: Option<CommitmentConfig>,
346 ) -> BoxFuture<Result<RpcResponse<UiTokenAmount>>>;
347}
348
349#[derive(Clone)]
350pub struct SurfpoolAccountsDataRpc;
351impl AccountsData for SurfpoolAccountsDataRpc {
352 type Metadata = Option<RunloopContext>;
353
354 fn get_account_info(
355 &self,
356 meta: Self::Metadata,
357 pubkey_str: String,
358 config: Option<RpcAccountInfoConfig>,
359 ) -> BoxFuture<Result<RpcResponse<Option<UiAccount>>>> {
360 let config = config.unwrap_or_default();
361 let pubkey = match verify_pubkey(&pubkey_str) {
362 Ok(res) => res,
363 Err(e) => return e.into(),
364 };
365
366 let SurfnetRpcContext {
367 svm_locker,
368 remote_ctx,
369 } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
370 Ok(res) => res,
371 Err(e) => return e.into(),
372 };
373
374 Box::pin(async move {
375 let SvmAccessContext {
376 slot,
377 inner: account_update,
378 ..
379 } = svm_locker.get_account(&remote_ctx, &pubkey, None).await?;
380 svm_locker.write_account_update(account_update.clone());
381
382 let SvmAccessContext {
383 inner: associated_data,
384 ..
385 } = svm_locker.get_local_account_associated_data(&account_update);
386
387 Ok(RpcResponse {
388 context: RpcResponseContext::new(slot),
389 value: account_update.try_into_ui_account(
390 config.encoding,
391 config.data_slice,
392 associated_data,
393 ),
394 })
395 })
396 }
397
398 fn get_multiple_accounts(
399 &self,
400 meta: Self::Metadata,
401 pubkeys_str: Vec<String>,
402 config: Option<RpcAccountInfoConfig>,
403 ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>> {
404 let config = config.unwrap_or_default();
405 let pubkeys = match pubkeys_str
406 .iter()
407 .map(|s| verify_pubkey(s))
408 .collect::<SurfpoolResult<Vec<_>>>()
409 {
410 Ok(p) => p,
411 Err(e) => return e.into(),
412 };
413
414 let SurfnetRpcContext {
415 svm_locker,
416 remote_ctx,
417 } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
418 Ok(res) => res,
419 Err(e) => return e.into(),
420 };
421
422 Box::pin(async move {
423 let SvmAccessContext {
424 slot,
425 inner: account_updates,
426 ..
427 } = svm_locker
428 .get_multiple_accounts(&remote_ctx, &pubkeys, None)
429 .await?;
430
431 svm_locker.write_multiple_account_updates(&account_updates);
432
433 let mut ui_accounts = vec![];
434 {
435 for account_update in account_updates.into_iter() {
436 ui_accounts.push(account_update.try_into_ui_account(
437 config.encoding,
438 config.data_slice,
439 None,
440 ));
441 }
442 }
443
444 Ok(RpcResponse {
445 context: RpcResponseContext::new(slot),
446 value: ui_accounts,
447 })
448 })
449 }
450
451 fn get_block_commitment(
452 &self,
453 meta: Self::Metadata,
454 block: Slot,
455 ) -> Result<RpcBlockCommitment<BlockCommitmentArray>> {
456 let (current_slot, block_exists) = meta
458 .with_svm_reader(|svm_reader| {
459 (
460 svm_reader.get_latest_absolute_slot(),
461 svm_reader.blocks.contains_key(&block),
462 )
463 })
464 .map_err(Into::<jsonrpc_core::Error>::into)?;
465
466 if !block_exists && block > current_slot {
468 return Err(jsonrpc_core::Error::invalid_params(format!(
469 "Block {} not found",
470 block
471 )));
472 }
473
474 let commitment_array = [0u64; 32];
475
476 Ok(RpcBlockCommitment {
477 commitment: Some(commitment_array),
478 total_stake: 0,
479 })
480 }
481
482 fn get_token_account_balance(
487 &self,
488 meta: Self::Metadata,
489 pubkey_str: String,
490 commitment: Option<CommitmentConfig>,
491 ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>> {
492 let pubkey = match verify_pubkey(&pubkey_str) {
493 Ok(res) => res,
494 Err(e) => return e.into(),
495 };
496
497 let SurfnetRpcContext {
498 svm_locker,
499 remote_ctx,
500 } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
501 Ok(res) => res,
502 Err(e) => return e.into(),
503 };
504
505 Box::pin(async move {
506 let token_account_result = svm_locker
507 .get_account(&remote_ctx, &pubkey, None)
508 .await?
509 .inner;
510
511 svm_locker.write_account_update(token_account_result.clone());
512
513 let token_account = token_account_result.map_account()?;
514
515 let unpacked_token_account =
516 TokenAccount::unpack(&token_account.data).map_err(|e| {
517 SurfpoolError::invalid_account_data(
518 pubkey,
519 "Invalid token account data",
520 Some(e.to_string()),
521 )
522 })?;
523
524 let SvmAccessContext {
525 slot,
526 inner: mint_account_result,
527 ..
528 } = svm_locker
529 .get_account(&remote_ctx, &unpacked_token_account.mint, None)
530 .await?;
531
532 svm_locker.write_account_update(mint_account_result.clone());
533
534 let mint_account = mint_account_result.map_account()?;
535 let unpacked_mint_account = Mint::unpack(&mint_account.data).map_err(|e| {
536 SurfpoolError::invalid_account_data(
537 unpacked_token_account.mint,
538 "Invalid token mint account data",
539 Some(e.to_string()),
540 )
541 })?;
542
543 let token_decimals = unpacked_mint_account.decimals;
544
545 Ok(RpcResponse {
546 context: RpcResponseContext::new(slot),
547 value: {
548 parse_token_v3(
549 &token_account.data,
550 Some(&SplTokenAdditionalDataV2 {
551 decimals: token_decimals,
552 ..Default::default()
553 }),
554 )
555 .ok()
556 .and_then(|t| match t {
557 TokenAccountType::Account(account) => Some(account.token_amount),
558 _ => None,
559 })
560 },
561 })
562 })
563 }
564
565 fn get_token_supply(
566 &self,
567 meta: Self::Metadata,
568 mint_str: String,
569 commitment: Option<CommitmentConfig>,
570 ) -> BoxFuture<Result<RpcResponse<UiTokenAmount>>> {
571 let mint_pubkey = match verify_pubkey(&mint_str) {
572 Ok(pubkey) => pubkey,
573 Err(e) => return e.into(),
574 };
575
576 let SurfnetRpcContext {
577 svm_locker,
578 remote_ctx,
579 } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
580 Ok(res) => res,
581 Err(e) => return e.into(),
582 };
583
584 Box::pin(async move {
585 let SvmAccessContext {
586 slot,
587 inner: mint_account_result,
588 ..
589 } = svm_locker
590 .get_account(&remote_ctx, &mint_pubkey, None)
591 .await?;
592
593 svm_locker.write_account_update(mint_account_result.clone());
594
595 let mint_account = mint_account_result.map_account()?;
596
597 if !matches!(mint_account.owner, owner if owner == spl_token::id() || owner == spl_token_2022::id())
598 {
599 return Err(SurfpoolError::invalid_account_data(
600 mint_pubkey,
601 "Account is not a token mint account",
602 None::<String>,
603 )
604 .into());
605 }
606
607 let mint_data = Mint::unpack(&mint_account.data).map_err(|e| {
608 SurfpoolError::invalid_account_data(
609 mint_pubkey,
610 "Invalid token mint account data",
611 Some(e.to_string()),
612 )
613 })?;
614
615 Ok(RpcResponse {
616 context: RpcResponseContext::new(slot),
617 value: {
618 parse_token_v3(
619 &mint_account.data,
620 Some(&SplTokenAdditionalDataV2 {
621 decimals: mint_data.decimals,
622 ..Default::default()
623 }),
624 )
625 .ok()
626 .and_then(|t| match t {
627 TokenAccountType::Mint(mint) => {
628 let supply_u64 = mint.supply.parse::<u64>().unwrap_or(0);
629 let ui_amount = if supply_u64 == 0 {
630 Some(0.0)
631 } else {
632 let divisor = 10_u64.pow(mint.decimals as u32);
633 Some(supply_u64 as f64 / divisor as f64)
634 };
635
636 Some(UiTokenAmount {
637 amount: mint.supply.clone(),
638 decimals: mint.decimals,
639 ui_amount,
640 ui_amount_string: mint.supply,
641 })
642 }
643 _ => None,
644 })
645 .ok_or_else(|| {
646 SurfpoolError::invalid_account_data(
647 mint_pubkey,
648 "Failed to parse token mint account",
649 None::<String>,
650 )
651 })?
652 },
653 })
654 })
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use solana_account::Account;
661 use solana_keypair::Keypair;
662 use solana_pubkey::Pubkey;
663 use solana_sdk::{
664 program_option::COption, program_pack::Pack, system_instruction::create_account,
665 };
666 use solana_signer::Signer;
667 use solana_transaction::Transaction;
668 use spl_associated_token_account::{
669 get_associated_token_address_with_program_id, instruction::create_associated_token_account,
670 };
671 use spl_token::state::{Account as TokenAccount, AccountState, Mint};
672 use spl_token_2022::{
673 extension::StateWithExtensions,
674 instruction::{initialize_mint2, mint_to, transfer_checked},
675 state::Account as Token2022Account,
676 };
677
678 use super::*;
679 use crate::{
680 surfnet::{remote::SurfnetRemoteClient, GetAccountResult},
681 tests::helpers::TestSetup,
682 };
683
684 #[ignore = "connection-required"]
685 #[tokio::test(flavor = "multi_thread")]
686 async fn test_get_token_account_balance() {
687 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
688
689 let mint_pk = Pubkey::new_unique();
690
691 let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
692 svm_reader
693 .inner
694 .minimum_balance_for_rent_exemption(Mint::LEN)
695 });
696
697 let mut data = [0; Mint::LEN];
698
699 let default = Mint {
700 decimals: 6,
701 supply: 1000000000000000,
702 is_initialized: true,
703 ..Default::default()
704 };
705 default.pack_into_slice(&mut data);
706
707 let mint_account = Account {
708 lamports: minimum_rent,
709 owner: spl_token::ID,
710 executable: false,
711 rent_epoch: 0,
712 data: data.to_vec(),
713 };
714
715 setup
716 .context
717 .svm_locker
718 .write_account_update(GetAccountResult::FoundAccount(mint_pk, mint_account, true));
719
720 let token_account_pk = Pubkey::new_unique();
721
722 let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
723 svm_reader
724 .inner
725 .minimum_balance_for_rent_exemption(TokenAccount::LEN)
726 });
727
728 let mut data = [0; TokenAccount::LEN];
729
730 let default = TokenAccount {
731 mint: mint_pk,
732 owner: spl_token::ID,
733 state: AccountState::Initialized,
734 amount: 100 * 1000000,
735 ..Default::default()
736 };
737 default.pack_into_slice(&mut data);
738
739 let token_account = Account {
740 lamports: minimum_rent,
741 owner: spl_token::ID,
742 executable: false,
743 rent_epoch: 0,
744 data: data.to_vec(),
745 };
746
747 setup
748 .context
749 .svm_locker
750 .write_account_update(GetAccountResult::FoundAccount(
751 token_account_pk,
752 token_account,
753 true,
754 ));
755
756 let res = setup
757 .rpc
758 .get_token_account_balance(Some(setup.context), token_account_pk.to_string(), None)
759 .await
760 .unwrap();
761
762 assert_eq!(
763 res.value.unwrap(),
764 UiTokenAmount {
765 amount: String::from("100000000"),
766 decimals: 6,
767 ui_amount: Some(100.0),
768 ui_amount_string: String::from("100")
769 }
770 );
771 }
772
773 #[test]
774 fn test_get_block_commitment_past_slot() {
775 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
776 let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
777 let past_slot = if current_slot > 10 {
778 current_slot - 10
779 } else {
780 0
781 };
782
783 let result = setup
784 .rpc
785 .get_block_commitment(Some(setup.context), past_slot)
786 .unwrap();
787
788 assert!(result.commitment.is_some());
790 assert_eq!(result.total_stake, 0);
791 }
792
793 #[test]
794 fn test_get_block_commitment_with_actual_block() {
795 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
796
797 let test_slot = 12345;
799 setup.context.svm_locker.with_svm_writer(|svm_writer| {
800 use crate::surfnet::BlockHeader;
801
802 svm_writer.blocks.insert(
803 test_slot,
804 BlockHeader {
805 hash: "test_hash".to_string(),
806 previous_blockhash: "prev_hash".to_string(),
807 parent_slot: test_slot - 1,
808 block_time: chrono::Utc::now().timestamp_millis(),
809 block_height: test_slot,
810 signatures: vec![],
811 },
812 );
813 });
814
815 let result = setup
816 .rpc
817 .get_block_commitment(Some(setup.context), test_slot)
818 .unwrap();
819
820 assert!(result.commitment.is_some());
822 assert_eq!(result.total_stake, 0);
823 }
824
825 #[test]
826 fn test_get_block_commitment_no_metadata() {
827 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
828
829 let result = setup.rpc.get_block_commitment(None, 123);
830
831 assert!(result.is_err());
832 }
834
835 #[test]
836 fn test_get_block_commitment_future_slot_error() {
837 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
838 let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
839 let future_slot = current_slot + 1000;
840
841 let result = setup
842 .rpc
843 .get_block_commitment(Some(setup.context), future_slot);
844
845 assert!(result.is_err());
847
848 let error = result.unwrap_err();
849 assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
850 assert!(error.message.contains("Block") && error.message.contains("not found"));
851 }
852
853 #[tokio::test(flavor = "multi_thread")]
854 async fn test_get_token_supply_with_real_mint() {
855 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
856
857 let mint_pubkey = Pubkey::new_unique();
858
859 let mut mint_data = [0u8; Mint::LEN];
861 let mint = Mint {
862 mint_authority: COption::Some(Pubkey::new_unique()),
863 supply: 1_000_000_000_000,
864 decimals: 6,
865 is_initialized: true,
866 freeze_authority: COption::None,
867 };
868 Mint::pack(mint, &mut mint_data).unwrap();
869
870 let mint_account = Account {
871 lamports: setup.context.svm_locker.with_svm_reader(|svm_reader| {
872 svm_reader
873 .inner
874 .minimum_balance_for_rent_exemption(Mint::LEN)
875 }),
876 data: mint_data.to_vec(),
877 owner: spl_token::id(),
878 executable: false,
879 rent_epoch: 0,
880 };
881
882 setup.context.svm_locker.with_svm_writer(|svm_writer| {
883 svm_writer
884 .set_account(&mint_pubkey, mint_account.clone())
885 .unwrap();
886 });
887
888 let res = setup
889 .rpc
890 .get_token_supply(
891 Some(setup.context),
892 mint_pubkey.to_string(),
893 Some(CommitmentConfig::confirmed()),
894 )
895 .await
896 .unwrap();
897
898 assert_eq!(res.value.amount, "1000000000000");
899 assert_eq!(res.value.decimals, 6);
900 assert_eq!(res.value.ui_amount_string, "1000000000000");
901 }
902
903 #[tokio::test(flavor = "multi_thread")]
904 async fn test_invalid_pubkey_format() {
905 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
906
907 let invalid_pubkeys = vec![
909 "",
910 "invalid",
911 "123",
912 "not-a-valid-base58-string!@#$",
913 "11111111111111111111111111111111111111111111111111111111111111111",
914 "invalid-base58-characters-ö",
915 ];
916
917 for invalid_pubkey in invalid_pubkeys {
918 let res = setup
919 .rpc
920 .get_token_supply(
921 Some(setup.context.clone()),
922 invalid_pubkey.to_string(),
923 Some(CommitmentConfig::confirmed()),
924 )
925 .await;
926
927 assert!(
928 res.is_err(),
929 "Should fail for invalid pubkey: '{}'",
930 invalid_pubkey
931 );
932
933 let error_msg = res.unwrap_err().to_string();
934 assert!(
935 error_msg.contains("Invalid") || error_msg.contains("invalid"),
936 "Error should mention invalidity for '{}': {}",
937 invalid_pubkey,
938 error_msg
939 );
940 }
941
942 println!("✅ All invalid pubkey formats correctly rejected");
943 }
944
945 #[tokio::test(flavor = "multi_thread")]
946 async fn test_nonexistent_account() {
947 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
948
949 let nonexistent_mint = Pubkey::new_unique();
951
952 let res = setup
953 .rpc
954 .get_token_supply(
955 Some(setup.context),
956 nonexistent_mint.to_string(),
957 Some(CommitmentConfig::confirmed()),
958 )
959 .await;
960
961 assert!(res.is_err(), "Should fail for nonexistent account");
962
963 let error_msg = res.unwrap_err().to_string();
964 assert!(
965 error_msg.contains("not found") || error_msg.contains("account"),
966 "Error should mention account not found: {}",
967 error_msg
968 );
969
970 println!("✅ Nonexistent account correctly rejected: {}", error_msg);
971 }
972
973 #[tokio::test(flavor = "multi_thread")]
974 async fn test_invalid_mint_data() {
975 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
976
977 let fake_mint = Pubkey::new_unique();
978
979 setup.context.svm_locker.with_svm_writer(|svm_writer| {
980 let invalid_mint_account = Account {
982 lamports: 1000000,
983 data: vec![0xFF; 50], owner: spl_token::id(),
985 executable: false,
986 rent_epoch: 0,
987 };
988
989 svm_writer
990 .set_account(&fake_mint, invalid_mint_account)
991 .unwrap();
992 });
993
994 let res = setup
995 .rpc
996 .get_token_supply(
997 Some(setup.context),
998 fake_mint.to_string(),
999 Some(CommitmentConfig::confirmed()),
1000 )
1001 .await;
1002
1003 assert!(
1004 res.is_err(),
1005 "Should fail for account with invalid mint data"
1006 );
1007
1008 let error_msg = res.unwrap_err().to_string();
1009 assert!(
1010 error_msg.contains("deserialize")
1011 || error_msg.contains("Invalid")
1012 || error_msg.contains("parse"),
1013 "Error should mention deserialization failure: {}",
1014 error_msg
1015 );
1016
1017 println!("✅ Invalid mint data correctly rejected: {}", error_msg);
1018 }
1019
1020 #[ignore = "requires-network"]
1021 #[tokio::test(flavor = "multi_thread")]
1022 async fn test_remote_rpc_failure() {
1023 let bad_remote_client =
1025 SurfnetRemoteClient::new("https://invalid-url-that-doesnt-exist.com");
1026 let mut setup = TestSetup::new(SurfpoolAccountsDataRpc);
1027 setup.context.remote_rpc_client = Some(bad_remote_client);
1028
1029 let nonexistent_mint = Pubkey::new_unique();
1030
1031 let res = setup
1032 .rpc
1033 .get_token_supply(
1034 Some(setup.context),
1035 nonexistent_mint.to_string(),
1036 Some(CommitmentConfig::confirmed()),
1037 )
1038 .await;
1039
1040 assert!(res.is_err(), "Should fail when remote RPC is unreachable");
1041
1042 let error_msg = res.unwrap_err().to_string();
1043 println!("✅ Remote RPC failure handled: {}", error_msg);
1044 }
1045
1046 #[tokio::test(flavor = "multi_thread")]
1047 async fn test_transfer_token() {
1048 let client = TestSetup::new(SurfpoolAccountsDataRpc);
1050 let recent_blockhash = client
1051 .context
1052 .svm_locker
1053 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1054
1055 let fee_payer = Keypair::new();
1057
1058 let recipient = Keypair::new();
1060
1061 client
1063 .context
1064 .svm_locker
1065 .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1066 .unwrap();
1067
1068 client
1070 .context
1071 .svm_locker
1072 .airdrop(&recipient.pubkey(), 1_000_000_000)
1073 .unwrap();
1074
1075 let mint = Keypair::new();
1077
1078 let mint_space = Mint::LEN;
1080 let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1081 svm_reader
1082 .inner
1083 .minimum_balance_for_rent_exemption(mint_space)
1084 });
1085
1086 let create_account_instruction = create_account(
1088 &fee_payer.pubkey(), &mint.pubkey(), mint_rent, mint_space as u64, &spl_token_2022::id(), );
1094
1095 let initialize_mint_instruction = initialize_mint2(
1097 &spl_token_2022::id(),
1098 &mint.pubkey(), &fee_payer.pubkey(), Some(&fee_payer.pubkey()), 2, )
1103 .unwrap();
1104
1105 let source_token_address = get_associated_token_address_with_program_id(
1107 &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1111
1112 let create_source_ata_instruction = create_associated_token_account(
1114 &fee_payer.pubkey(), &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1119
1120 let destination_token_address = get_associated_token_address_with_program_id(
1122 &recipient.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1126
1127 let create_destination_ata_instruction = create_associated_token_account(
1129 &fee_payer.pubkey(), &recipient.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1134
1135 let amount = 100_00;
1137
1138 let mint_to_instruction = mint_to(
1140 &spl_token_2022::id(),
1141 &mint.pubkey(), &source_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], amount, )
1147 .unwrap();
1148
1149 let transaction = Transaction::new_signed_with_payer(
1151 &[
1152 create_account_instruction,
1153 initialize_mint_instruction,
1154 create_source_ata_instruction,
1155 create_destination_ata_instruction,
1156 mint_to_instruction,
1157 ],
1158 Some(&fee_payer.pubkey()),
1159 &[&fee_payer, &mint],
1160 recent_blockhash,
1161 );
1162
1163 client.context.svm_locker.with_svm_writer(|svm_writer| {
1165 svm_writer
1166 .send_transaction(transaction.clone().into(), false)
1167 .unwrap();
1168 });
1169
1170 println!("Mint Address: {}", mint.pubkey());
1171 println!("Recipient Address: {}", recipient.pubkey());
1172 println!("Source Token Account Address: {}", source_token_address);
1173 println!(
1174 "Destination Token Account Address: {}",
1175 destination_token_address
1176 );
1177 println!("Minted {} tokens to the source token account", amount);
1178
1179 let recent_blockhash = client
1181 .context
1182 .svm_locker
1183 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1184
1185 let transfer_amount = 50;
1187
1188 let transfer_instruction = transfer_checked(
1190 &spl_token_2022::id(), &source_token_address, &mint.pubkey(), &destination_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], transfer_amount, 2, )
1199 .unwrap();
1200
1201 let transaction = Transaction::new_signed_with_payer(
1203 &[transfer_instruction],
1204 Some(&fee_payer.pubkey()),
1205 &[&fee_payer],
1206 recent_blockhash,
1207 );
1208
1209 client.context.svm_locker.with_svm_writer(|svm_writer| {
1211 svm_writer
1212 .send_transaction(transaction.clone().into(), false)
1213 .unwrap();
1214 });
1215
1216 println!("Successfully transferred 0.50 tokens from sender to recipient");
1217
1218 let source_token_account = client
1220 .context
1221 .svm_locker
1222 .get_account_local(&source_token_address)
1223 .inner;
1224 let destination_token_account = client
1225 .context
1226 .svm_locker
1227 .get_account_local(&destination_token_address)
1228 .inner;
1229
1230 if let GetAccountResult::FoundAccount(_, source_account, _) = source_token_account {
1231 let unpacked =
1232 StateWithExtensions::<Token2022Account>::unpack(&source_account.data).unwrap();
1233 println!(
1234 "Source Token Account Balance: {} tokens",
1235 unpacked.base.amount
1236 );
1237 assert_eq!(unpacked.base.amount, 9950);
1238 }
1239
1240 if let GetAccountResult::FoundAccount(_, destination_account, _) = destination_token_account
1241 {
1242 let unpacked =
1243 StateWithExtensions::<Token2022Account>::unpack(&destination_account.data).unwrap();
1244 println!(
1245 "Destination Token Account Balance: {} tokens",
1246 unpacked.base.amount
1247 );
1248 assert_eq!(unpacked.base.amount, 50);
1249 }
1250 }
1251}