1use jsonrpc_core::{BoxFuture, Result};
2use jsonrpc_derive::rpc;
3use solana_account_decoder::{
4 UiAccount,
5 parse_account_data::SplTokenAdditionalDataV2,
6 parse_token::{TokenAccountType, UiTokenAmount, parse_token_v3},
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;
16
17use super::{RunloopContext, SurfnetRpcContext};
18use crate::{
19 error::{SurfpoolError, SurfpoolResult},
20 rpc::{State, utils::verify_pubkey},
21 surfnet::locker::{SvmAccessContext, is_supported_token_program},
22 types::{MintAccount, TokenAccount},
23};
24
25#[rpc]
26pub trait AccountsData {
27 type Metadata;
28
29 #[rpc(meta, name = "getAccountInfo")]
91 fn get_account_info(
92 &self,
93 meta: Self::Metadata,
94 pubkey_str: String,
95 config: Option<RpcAccountInfoConfig>,
96 ) -> BoxFuture<Result<RpcResponse<Option<UiAccount>>>>;
97
98 #[rpc(meta, name = "getBlockCommitment")]
142 fn get_block_commitment(
143 &self,
144 meta: Self::Metadata,
145 block: Slot,
146 ) -> Result<RpcBlockCommitment<BlockCommitmentArray>>;
147
148 #[rpc(meta, name = "getMultipleAccounts")]
216 fn get_multiple_accounts(
217 &self,
218 meta: Self::Metadata,
219 pubkey_strs: Vec<String>,
220 config: Option<RpcAccountInfoConfig>,
221 ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>>;
222
223 #[rpc(meta, name = "getTokenAccountBalance")]
278 fn get_token_account_balance(
279 &self,
280 meta: Self::Metadata,
281 pubkey_str: String,
282 commitment: Option<CommitmentConfig>,
283 ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>>;
284
285 #[rpc(meta, name = "getTokenSupply")]
340 fn get_token_supply(
341 &self,
342 meta: Self::Metadata,
343 mint_str: String,
344 commitment: Option<CommitmentConfig>,
345 ) -> BoxFuture<Result<RpcResponse<UiTokenAmount>>>;
346}
347
348#[derive(Clone)]
349pub struct SurfpoolAccountsDataRpc;
350impl AccountsData for SurfpoolAccountsDataRpc {
351 type Metadata = Option<RunloopContext>;
352
353 fn get_account_info(
354 &self,
355 meta: Self::Metadata,
356 pubkey_str: String,
357 config: Option<RpcAccountInfoConfig>,
358 ) -> BoxFuture<Result<RpcResponse<Option<UiAccount>>>> {
359 let config = config.unwrap_or_default();
360 let pubkey = match verify_pubkey(&pubkey_str) {
361 Ok(res) => res,
362 Err(e) => return e.into(),
363 };
364
365 let SurfnetRpcContext {
366 svm_locker,
367 remote_ctx,
368 } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
369 Ok(res) => res,
370 Err(e) => return e.into(),
371 };
372
373 Box::pin(async move {
374 let SvmAccessContext {
375 slot,
376 inner: account_update,
377 ..
378 } = svm_locker.get_account(&remote_ctx, &pubkey, None).await?;
379 svm_locker.write_account_update(account_update.clone());
380
381 let ui_account = if let Some(((pubkey, account), token_data)) =
382 account_update.map_account_with_token_data()
383 {
384 Some(
385 svm_locker
386 .account_to_rpc_keyed_account(
387 &pubkey,
388 &account,
389 &config,
390 token_data.map(|(mint, _)| mint),
391 )
392 .account,
393 )
394 } else {
395 None
396 };
397
398 Ok(RpcResponse {
399 context: RpcResponseContext::new(slot),
400 value: ui_account,
401 })
402 })
403 }
404
405 fn get_multiple_accounts(
406 &self,
407 meta: Self::Metadata,
408 pubkeys_str: Vec<String>,
409 config: Option<RpcAccountInfoConfig>,
410 ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>> {
411 let config = config.unwrap_or_default();
412 let pubkeys = match pubkeys_str
413 .iter()
414 .map(|s| verify_pubkey(s))
415 .collect::<SurfpoolResult<Vec<_>>>()
416 {
417 Ok(p) => p,
418 Err(e) => return e.into(),
419 };
420
421 let SurfnetRpcContext {
422 svm_locker,
423 remote_ctx,
424 } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
425 Ok(res) => res,
426 Err(e) => return e.into(),
427 };
428
429 Box::pin(async move {
430 let SvmAccessContext {
431 slot,
432 inner: account_updates,
433 ..
434 } = svm_locker
435 .get_multiple_accounts(&remote_ctx, &pubkeys, None)
436 .await?;
437
438 svm_locker.write_multiple_account_updates(&account_updates);
439
440 let mut ui_accounts = vec![];
441
442 for account_update in account_updates.into_iter() {
443 if let Some(((pubkey, account), token_data)) =
444 account_update.map_account_with_token_data()
445 {
446 ui_accounts.push(Some(
447 svm_locker
448 .account_to_rpc_keyed_account(
449 &pubkey,
450 &account,
451 &config,
452 token_data.map(|(mint, _)| mint),
453 )
454 .account,
455 ));
456 } else {
457 ui_accounts.push(None);
458 };
459 }
460
461 Ok(RpcResponse {
462 context: RpcResponseContext::new(slot),
463 value: ui_accounts,
464 })
465 })
466 }
467
468 fn get_block_commitment(
469 &self,
470 meta: Self::Metadata,
471 block: Slot,
472 ) -> Result<RpcBlockCommitment<BlockCommitmentArray>> {
473 let (current_slot, block_exists) = meta
475 .with_svm_reader(|svm_reader| {
476 (
477 svm_reader.get_latest_absolute_slot(),
478 svm_reader.blocks.contains_key(&block),
479 )
480 })
481 .map_err(Into::<jsonrpc_core::Error>::into)?;
482
483 if !block_exists && block > current_slot {
485 return Err(jsonrpc_core::Error::invalid_params(format!(
486 "Block {} not found",
487 block
488 )));
489 }
490
491 let commitment_array = [0u64; 32];
492
493 Ok(RpcBlockCommitment {
494 commitment: Some(commitment_array),
495 total_stake: 0,
496 })
497 }
498
499 fn get_token_account_balance(
504 &self,
505 meta: Self::Metadata,
506 pubkey_str: String,
507 commitment: Option<CommitmentConfig>,
508 ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>> {
509 let pubkey = match verify_pubkey(&pubkey_str) {
510 Ok(res) => res,
511 Err(e) => return e.into(),
512 };
513
514 let SurfnetRpcContext {
515 svm_locker,
516 remote_ctx,
517 } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
518 Ok(res) => res,
519 Err(e) => return e.into(),
520 };
521
522 Box::pin(async move {
523 let token_account_result = svm_locker
524 .get_account(&remote_ctx, &pubkey, None)
525 .await?
526 .inner;
527
528 svm_locker.write_account_update(token_account_result.clone());
529
530 let token_account = token_account_result.map_account()?;
531
532 let (mint_pubkey, _amount) = if is_supported_token_program(&token_account.owner) {
533 let unpacked_token_account = TokenAccount::unpack(&token_account.data)?;
534 (
535 unpacked_token_account.mint(),
536 unpacked_token_account.amount(),
537 )
538 } else {
539 return Err(SurfpoolError::invalid_account_data(
540 pubkey,
541 "Account is not owned by Token or Token-2022 program",
542 None::<String>,
543 )
544 .into());
545 };
546
547 let SvmAccessContext {
548 slot,
549 inner: mint_account_result,
550 ..
551 } = svm_locker
552 .get_account(&remote_ctx, &mint_pubkey, None)
553 .await?;
554
555 svm_locker.write_account_update(mint_account_result.clone());
556
557 let mint_account = mint_account_result.map_account()?;
558
559 let token_decimals = if is_supported_token_program(&mint_account.owner) {
560 let unpacked_mint_account = MintAccount::unpack(&mint_account.data)?;
561 unpacked_mint_account.decimals()
562 } else {
563 return Err(SurfpoolError::invalid_account_data(
564 mint_pubkey,
565 "Mint account is not owned by Token or Token-2022 program",
566 None::<String>,
567 )
568 .into());
569 };
570
571 Ok(RpcResponse {
572 context: RpcResponseContext::new(slot),
573 value: {
574 parse_token_v3(
575 &token_account.data,
576 Some(&SplTokenAdditionalDataV2 {
577 decimals: token_decimals,
578 ..Default::default()
579 }),
580 )
581 .ok()
582 .and_then(|t| match t {
583 TokenAccountType::Account(account) => Some(account.token_amount),
584 _ => None,
585 })
586 },
587 })
588 })
589 }
590
591 fn get_token_supply(
592 &self,
593 meta: Self::Metadata,
594 mint_str: String,
595 commitment: Option<CommitmentConfig>,
596 ) -> BoxFuture<Result<RpcResponse<UiTokenAmount>>> {
597 let mint_pubkey = match verify_pubkey(&mint_str) {
598 Ok(pubkey) => pubkey,
599 Err(e) => return e.into(),
600 };
601
602 let SurfnetRpcContext {
603 svm_locker,
604 remote_ctx,
605 } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
606 Ok(res) => res,
607 Err(e) => return e.into(),
608 };
609
610 Box::pin(async move {
611 let SvmAccessContext {
612 slot,
613 inner: mint_account_result,
614 ..
615 } = svm_locker
616 .get_account(&remote_ctx, &mint_pubkey, None)
617 .await?;
618
619 svm_locker.write_account_update(mint_account_result.clone());
620
621 let mint_account = mint_account_result.map_account()?;
622
623 if !is_supported_token_program(&mint_account.owner) {
624 return Err(SurfpoolError::invalid_account_data(
625 mint_pubkey,
626 "Account is not a token mint account",
627 None::<String>,
628 )
629 .into());
630 }
631
632 let mint_data = MintAccount::unpack(&mint_account.data)?;
633
634 Ok(RpcResponse {
635 context: RpcResponseContext::new(slot),
636 value: {
637 parse_token_v3(
638 &mint_account.data,
639 Some(&SplTokenAdditionalDataV2 {
640 decimals: mint_data.decimals(),
641 ..Default::default()
642 }),
643 )
644 .ok()
645 .and_then(|t| match t {
646 TokenAccountType::Mint(mint) => {
647 let supply_u64 = mint.supply.parse::<u64>().unwrap_or(0);
648 let ui_amount = if supply_u64 == 0 {
649 Some(0.0)
650 } else {
651 let divisor = 10_u64.pow(mint.decimals as u32);
652 Some(supply_u64 as f64 / divisor as f64)
653 };
654
655 Some(UiTokenAmount {
656 amount: mint.supply.clone(),
657 decimals: mint.decimals,
658 ui_amount,
659 ui_amount_string: mint.supply,
660 })
661 }
662 _ => None,
663 })
664 .ok_or_else(|| {
665 SurfpoolError::invalid_account_data(
666 mint_pubkey,
667 "Failed to parse token mint account",
668 None::<String>,
669 )
670 })?
671 },
672 })
673 })
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use solana_account::Account;
680 use solana_keypair::Keypair;
681 use solana_pubkey::Pubkey;
682 use solana_sdk::{
683 program_option::COption, program_pack::Pack, system_instruction::create_account,
684 };
685 use solana_signer::Signer;
686 use solana_transaction::Transaction;
687 use spl_associated_token_account::{
688 get_associated_token_address_with_program_id, instruction::create_associated_token_account,
689 };
690 use spl_token::state::{Account as TokenAccount, AccountState, Mint};
691 use spl_token_2022::instruction::{initialize_mint2, mint_to, transfer_checked};
692
693 use super::*;
694 use crate::{
695 surfnet::{GetAccountResult, remote::SurfnetRemoteClient},
696 tests::helpers::TestSetup,
697 };
698
699 #[ignore = "connection-required"]
700 #[tokio::test(flavor = "multi_thread")]
701 async fn test_get_token_account_balance() {
702 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
703
704 let mint_pk = Pubkey::new_unique();
705
706 let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
707 svm_reader
708 .inner
709 .minimum_balance_for_rent_exemption(Mint::LEN)
710 });
711
712 let mut data = [0; Mint::LEN];
713
714 let default = Mint {
715 decimals: 6,
716 supply: 1000000000000000,
717 is_initialized: true,
718 ..Default::default()
719 };
720 default.pack_into_slice(&mut data);
721
722 let mint_account = Account {
723 lamports: minimum_rent,
724 owner: spl_token::ID,
725 executable: false,
726 rent_epoch: 0,
727 data: data.to_vec(),
728 };
729
730 setup
731 .context
732 .svm_locker
733 .write_account_update(GetAccountResult::FoundAccount(mint_pk, mint_account, true));
734
735 let token_account_pk = Pubkey::new_unique();
736
737 let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
738 svm_reader
739 .inner
740 .minimum_balance_for_rent_exemption(TokenAccount::LEN)
741 });
742
743 let mut data = [0; TokenAccount::LEN];
744
745 let default = TokenAccount {
746 mint: mint_pk,
747 owner: spl_token::ID,
748 state: AccountState::Initialized,
749 amount: 100 * 1000000,
750 ..Default::default()
751 };
752 default.pack_into_slice(&mut data);
753
754 let token_account = Account {
755 lamports: minimum_rent,
756 owner: spl_token::ID,
757 executable: false,
758 rent_epoch: 0,
759 data: data.to_vec(),
760 };
761
762 setup
763 .context
764 .svm_locker
765 .write_account_update(GetAccountResult::FoundAccount(
766 token_account_pk,
767 token_account,
768 true,
769 ));
770
771 let res = setup
772 .rpc
773 .get_token_account_balance(Some(setup.context), token_account_pk.to_string(), None)
774 .await
775 .unwrap();
776
777 assert_eq!(
778 res.value.unwrap(),
779 UiTokenAmount {
780 amount: String::from("100000000"),
781 decimals: 6,
782 ui_amount: Some(100.0),
783 ui_amount_string: String::from("100")
784 }
785 );
786 }
787
788 #[test]
789 fn test_get_block_commitment_past_slot() {
790 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
791 let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
792 let past_slot = if current_slot > 10 {
793 current_slot - 10
794 } else {
795 0
796 };
797
798 let result = setup
799 .rpc
800 .get_block_commitment(Some(setup.context), past_slot)
801 .unwrap();
802
803 assert!(result.commitment.is_some());
805 assert_eq!(result.total_stake, 0);
806 }
807
808 #[test]
809 fn test_get_block_commitment_with_actual_block() {
810 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
811
812 let test_slot = 12345;
814 setup.context.svm_locker.with_svm_writer(|svm_writer| {
815 use crate::surfnet::BlockHeader;
816
817 svm_writer.blocks.insert(
818 test_slot,
819 BlockHeader {
820 hash: "test_hash".to_string(),
821 previous_blockhash: "prev_hash".to_string(),
822 parent_slot: test_slot - 1,
823 block_time: chrono::Utc::now().timestamp_millis(),
824 block_height: test_slot,
825 signatures: vec![],
826 },
827 );
828 });
829
830 let result = setup
831 .rpc
832 .get_block_commitment(Some(setup.context), test_slot)
833 .unwrap();
834
835 assert!(result.commitment.is_some());
837 assert_eq!(result.total_stake, 0);
838 }
839
840 #[test]
841 fn test_get_block_commitment_no_metadata() {
842 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
843
844 let result = setup.rpc.get_block_commitment(None, 123);
845
846 assert!(result.is_err());
847 }
849
850 #[test]
851 fn test_get_block_commitment_future_slot_error() {
852 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
853 let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
854 let future_slot = current_slot + 1000;
855
856 let result = setup
857 .rpc
858 .get_block_commitment(Some(setup.context), future_slot);
859
860 assert!(result.is_err());
862
863 let error = result.unwrap_err();
864 assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
865 assert!(error.message.contains("Block") && error.message.contains("not found"));
866 }
867
868 #[tokio::test(flavor = "multi_thread")]
869 async fn test_get_token_supply_with_real_mint() {
870 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
871
872 let mint_pubkey = Pubkey::new_unique();
873
874 let mut mint_data = [0u8; Mint::LEN];
876 let mint = Mint {
877 mint_authority: COption::Some(Pubkey::new_unique()),
878 supply: 1_000_000_000_000,
879 decimals: 6,
880 is_initialized: true,
881 freeze_authority: COption::None,
882 };
883 Mint::pack(mint, &mut mint_data).unwrap();
884
885 let mint_account = Account {
886 lamports: setup.context.svm_locker.with_svm_reader(|svm_reader| {
887 svm_reader
888 .inner
889 .minimum_balance_for_rent_exemption(Mint::LEN)
890 }),
891 data: mint_data.to_vec(),
892 owner: spl_token::id(),
893 executable: false,
894 rent_epoch: 0,
895 };
896
897 setup.context.svm_locker.with_svm_writer(|svm_writer| {
898 svm_writer
899 .set_account(&mint_pubkey, mint_account.clone())
900 .unwrap();
901 });
902
903 let res = setup
904 .rpc
905 .get_token_supply(
906 Some(setup.context),
907 mint_pubkey.to_string(),
908 Some(CommitmentConfig::confirmed()),
909 )
910 .await
911 .unwrap();
912
913 assert_eq!(res.value.amount, "1000000000000");
914 assert_eq!(res.value.decimals, 6);
915 assert_eq!(res.value.ui_amount_string, "1000000000000");
916 }
917
918 #[tokio::test(flavor = "multi_thread")]
919 async fn test_invalid_pubkey_format() {
920 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
921
922 let invalid_pubkeys = vec![
924 "",
925 "invalid",
926 "123",
927 "not-a-valid-base58-string!@#$",
928 "11111111111111111111111111111111111111111111111111111111111111111",
929 "invalid-base58-characters-ö",
930 ];
931
932 for invalid_pubkey in invalid_pubkeys {
933 let res = setup
934 .rpc
935 .get_token_supply(
936 Some(setup.context.clone()),
937 invalid_pubkey.to_string(),
938 Some(CommitmentConfig::confirmed()),
939 )
940 .await;
941
942 assert!(
943 res.is_err(),
944 "Should fail for invalid pubkey: '{}'",
945 invalid_pubkey
946 );
947
948 let error_msg = res.unwrap_err().to_string();
949 assert!(
950 error_msg.contains("Invalid") || error_msg.contains("invalid"),
951 "Error should mention invalidity for '{}': {}",
952 invalid_pubkey,
953 error_msg
954 );
955 }
956
957 println!("✅ All invalid pubkey formats correctly rejected");
958 }
959
960 #[tokio::test(flavor = "multi_thread")]
961 async fn test_nonexistent_account() {
962 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
963
964 let nonexistent_mint = Pubkey::new_unique();
966
967 let res = setup
968 .rpc
969 .get_token_supply(
970 Some(setup.context),
971 nonexistent_mint.to_string(),
972 Some(CommitmentConfig::confirmed()),
973 )
974 .await;
975
976 assert!(res.is_err(), "Should fail for nonexistent account");
977
978 let error_msg = res.unwrap_err().to_string();
979 assert!(
980 error_msg.contains("not found") || error_msg.contains("account"),
981 "Error should mention account not found: {}",
982 error_msg
983 );
984
985 println!("✅ Nonexistent account correctly rejected: {}", error_msg);
986 }
987
988 #[tokio::test(flavor = "multi_thread")]
989 async fn test_invalid_mint_data() {
990 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
991
992 let fake_mint = Pubkey::new_unique();
993
994 setup.context.svm_locker.with_svm_writer(|svm_writer| {
995 let invalid_mint_account = Account {
997 lamports: 1000000,
998 data: vec![0xFF; 50], owner: spl_token::id(),
1000 executable: false,
1001 rent_epoch: 0,
1002 };
1003
1004 svm_writer
1005 .set_account(&fake_mint, invalid_mint_account)
1006 .unwrap();
1007 });
1008
1009 let res = setup
1010 .rpc
1011 .get_token_supply(
1012 Some(setup.context),
1013 fake_mint.to_string(),
1014 Some(CommitmentConfig::confirmed()),
1015 )
1016 .await;
1017
1018 assert!(
1019 res.is_err(),
1020 "Should fail for account with invalid mint data"
1021 );
1022
1023 let error_msg = res.unwrap_err().to_string();
1024 assert!(
1025 error_msg.eq("Parse error: Failed to unpack mint account"),
1026 "Incorrect error received: {}",
1027 error_msg
1028 );
1029
1030 println!("✅ Invalid mint data correctly rejected: {}", error_msg);
1031 }
1032
1033 #[ignore = "requires-network"]
1034 #[tokio::test(flavor = "multi_thread")]
1035 async fn test_remote_rpc_failure() {
1036 let bad_remote_client =
1038 SurfnetRemoteClient::new("https://invalid-url-that-doesnt-exist.com");
1039 let mut setup = TestSetup::new(SurfpoolAccountsDataRpc);
1040 setup.context.remote_rpc_client = Some(bad_remote_client);
1041
1042 let nonexistent_mint = Pubkey::new_unique();
1043
1044 let res = setup
1045 .rpc
1046 .get_token_supply(
1047 Some(setup.context),
1048 nonexistent_mint.to_string(),
1049 Some(CommitmentConfig::confirmed()),
1050 )
1051 .await;
1052
1053 assert!(res.is_err(), "Should fail when remote RPC is unreachable");
1054
1055 let error_msg = res.unwrap_err().to_string();
1056 println!("✅ Remote RPC failure handled: {}", error_msg);
1057 }
1058
1059 #[tokio::test(flavor = "multi_thread")]
1060 async fn test_transfer_token() {
1061 let client = TestSetup::new(SurfpoolAccountsDataRpc);
1063 let recent_blockhash = client
1064 .context
1065 .svm_locker
1066 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1067
1068 let fee_payer = Keypair::new();
1070
1071 let recipient = Keypair::new();
1073
1074 client
1076 .context
1077 .svm_locker
1078 .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1079 .unwrap();
1080
1081 client
1083 .context
1084 .svm_locker
1085 .airdrop(&recipient.pubkey(), 1_000_000_000)
1086 .unwrap();
1087
1088 let mint = Keypair::new();
1090
1091 let mint_space = Mint::LEN;
1093 let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1094 svm_reader
1095 .inner
1096 .minimum_balance_for_rent_exemption(mint_space)
1097 });
1098
1099 let create_account_instruction = create_account(
1101 &fee_payer.pubkey(), &mint.pubkey(), mint_rent, mint_space as u64, &spl_token_2022::id(), );
1107
1108 let initialize_mint_instruction = initialize_mint2(
1110 &spl_token_2022::id(),
1111 &mint.pubkey(), &fee_payer.pubkey(), Some(&fee_payer.pubkey()), 2, )
1116 .unwrap();
1117
1118 let source_token_address = get_associated_token_address_with_program_id(
1120 &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1124
1125 let create_source_ata_instruction = create_associated_token_account(
1127 &fee_payer.pubkey(), &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1132
1133 let destination_token_address = get_associated_token_address_with_program_id(
1135 &recipient.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1139
1140 let create_destination_ata_instruction = create_associated_token_account(
1142 &fee_payer.pubkey(), &recipient.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1147
1148 let amount = 100_00;
1150
1151 let mint_to_instruction = mint_to(
1153 &spl_token_2022::id(),
1154 &mint.pubkey(), &source_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], amount, )
1160 .unwrap();
1161
1162 let transaction = Transaction::new_signed_with_payer(
1164 &[
1165 create_account_instruction,
1166 initialize_mint_instruction,
1167 create_source_ata_instruction,
1168 create_destination_ata_instruction,
1169 mint_to_instruction,
1170 ],
1171 Some(&fee_payer.pubkey()),
1172 &[&fee_payer, &mint],
1173 recent_blockhash,
1174 );
1175
1176 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1177 client
1178 .context
1179 .svm_locker
1180 .process_transaction(&None, transaction.into(), status_tx.clone(), false)
1181 .await
1182 .unwrap();
1183
1184 println!("Mint Address: {}", mint.pubkey());
1185 println!("Recipient Address: {}", recipient.pubkey());
1186 println!("Source Token Account Address: {}", source_token_address);
1187 println!(
1188 "Destination Token Account Address: {}",
1189 destination_token_address
1190 );
1191 println!("Minted {} tokens to the source token account", amount);
1192
1193 let recent_blockhash = client
1195 .context
1196 .svm_locker
1197 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1198
1199 let transfer_amount = 50;
1201
1202 let transfer_instruction = transfer_checked(
1204 &spl_token_2022::id(), &source_token_address, &mint.pubkey(), &destination_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], transfer_amount, 2, )
1213 .unwrap();
1214
1215 let transaction = Transaction::new_signed_with_payer(
1217 &[transfer_instruction],
1218 Some(&fee_payer.pubkey()),
1219 &[&fee_payer],
1220 recent_blockhash,
1221 );
1222
1223 client
1224 .context
1225 .svm_locker
1226 .process_transaction(&None, transaction.clone().into(), status_tx, true)
1227 .await
1228 .unwrap();
1229
1230 println!("Successfully transferred 0.50 tokens from sender to recipient");
1231
1232 let source_balance = client
1233 .rpc
1234 .get_token_account_balance(
1235 Some(client.context.clone()),
1236 source_token_address.to_string(),
1237 Some(CommitmentConfig::confirmed()),
1238 )
1239 .await
1240 .unwrap();
1241
1242 let destination_balance = client
1243 .rpc
1244 .get_token_account_balance(
1245 Some(client.context.clone()),
1246 destination_token_address.to_string(),
1247 Some(CommitmentConfig::confirmed()),
1248 )
1249 .await
1250 .unwrap();
1251
1252 println!(
1253 "Source Token Account Balance: {} tokens ({})",
1254 source_balance.value.as_ref().unwrap().ui_amount.unwrap(),
1255 source_balance.value.as_ref().unwrap().amount
1256 );
1257 println!(
1258 "Destination Token Account Balance: {} tokens ({})",
1259 destination_balance
1260 .value
1261 .as_ref()
1262 .unwrap()
1263 .ui_amount
1264 .unwrap(),
1265 destination_balance.value.as_ref().unwrap().amount
1266 );
1267
1268 assert_eq!(source_balance.value.unwrap().amount, "9950");
1269 assert_eq!(destination_balance.value.unwrap().amount, "50");
1270 }
1271
1272 #[tokio::test(flavor = "multi_thread")]
1273 async fn test_get_account_info() {
1274 let client = TestSetup::new(SurfpoolAccountsDataRpc);
1276 let recent_blockhash = client
1277 .context
1278 .svm_locker
1279 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1280
1281 let fee_payer = Keypair::new();
1283
1284 let recipient = Keypair::new();
1286
1287 client
1289 .context
1290 .svm_locker
1291 .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1292 .unwrap();
1293
1294 client
1296 .context
1297 .svm_locker
1298 .airdrop(&recipient.pubkey(), 1_000_000_000)
1299 .unwrap();
1300
1301 let mint = Keypair::new();
1303
1304 let mint_space = Mint::LEN;
1306 let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1307 svm_reader
1308 .inner
1309 .minimum_balance_for_rent_exemption(mint_space)
1310 });
1311
1312 let create_account_instruction = create_account(
1314 &fee_payer.pubkey(), &mint.pubkey(), mint_rent, mint_space as u64, &spl_token_2022::id(), );
1320
1321 let initialize_mint_instruction = initialize_mint2(
1323 &spl_token_2022::id(),
1324 &mint.pubkey(), &fee_payer.pubkey(), Some(&fee_payer.pubkey()), 2, )
1329 .unwrap();
1330
1331 let source_token_address = get_associated_token_address_with_program_id(
1333 &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1337
1338 let create_source_ata_instruction = create_associated_token_account(
1340 &fee_payer.pubkey(), &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1345
1346 let destination_token_address = get_associated_token_address_with_program_id(
1348 &recipient.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1352
1353 let create_destination_ata_instruction = create_associated_token_account(
1355 &fee_payer.pubkey(), &recipient.pubkey(), &mint.pubkey(), &spl_token_2022::id(), );
1360
1361 let amount = 100_00;
1363
1364 let mint_to_instruction = mint_to(
1366 &spl_token_2022::id(),
1367 &mint.pubkey(), &source_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], amount, )
1373 .unwrap();
1374
1375 let transaction = Transaction::new_signed_with_payer(
1377 &[
1378 create_account_instruction,
1379 initialize_mint_instruction,
1380 create_source_ata_instruction,
1381 create_destination_ata_instruction,
1382 mint_to_instruction,
1383 ],
1384 Some(&fee_payer.pubkey()),
1385 &[&fee_payer, &mint],
1386 recent_blockhash,
1387 );
1388
1389 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1390 client
1392 .context
1393 .svm_locker
1394 .process_transaction(&None, transaction.clone().into(), status_tx, true)
1395 .await
1396 .unwrap();
1397
1398 println!("Mint Address: {}", mint.pubkey());
1399 println!("Recipient Address: {}", recipient.pubkey());
1400 println!("Source Token Account Address: {}", source_token_address);
1401 println!(
1402 "Destination Token Account Address: {}",
1403 destination_token_address
1404 );
1405 println!("Minted {} tokens to the source token account", amount);
1406
1407 let recent_blockhash = client
1409 .context
1410 .svm_locker
1411 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1412
1413 let transfer_amount = 50;
1415
1416 let transfer_instruction = transfer_checked(
1418 &spl_token_2022::id(), &source_token_address, &mint.pubkey(), &destination_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], transfer_amount, 2, )
1427 .unwrap();
1428
1429 let transaction = Transaction::new_signed_with_payer(
1431 &[transfer_instruction],
1432 Some(&fee_payer.pubkey()),
1433 &[&fee_payer],
1434 recent_blockhash,
1435 );
1436 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1437 client
1439 .context
1440 .svm_locker
1441 .process_transaction(&None, transaction.clone().into(), status_tx, true)
1442 .await
1443 .unwrap();
1444
1445 println!(
1446 "Successfully transferred 0.50 tokens from {} to {}",
1447 source_token_address, destination_token_address
1448 );
1449
1450 let source_account_info = client
1451 .rpc
1452 .get_account_info(
1453 Some(client.context.clone()),
1454 source_token_address.to_string(),
1455 Some(RpcAccountInfoConfig {
1456 encoding: Some(solana_account_decoder::UiAccountEncoding::JsonParsed),
1457 ..Default::default()
1458 }),
1459 )
1460 .await
1461 .unwrap();
1462
1463 let destination_account_info = client
1464 .rpc
1465 .get_account_info(
1466 Some(client.context.clone()),
1467 destination_token_address.to_string(),
1468 Some(RpcAccountInfoConfig {
1469 encoding: Some(solana_account_decoder::UiAccountEncoding::JsonParsed),
1470 ..Default::default()
1471 }),
1472 )
1473 .await
1474 .unwrap();
1475
1476 println!("Source Account Info: {:?}", source_account_info);
1477 println!("Destination Account Info: {:?}", destination_account_info);
1478
1479 let source_account = source_account_info.value.unwrap();
1480 if let solana_account_decoder::UiAccountData::Json(parsed) = source_account.data {
1481 let amount = parsed.parsed["info"]["tokenAmount"]["amount"]
1482 .as_str()
1483 .unwrap();
1484 assert_eq!(amount, "9950");
1485 } else {
1486 panic!("source account data was not in json parsed format");
1487 }
1488
1489 let destination_account = destination_account_info.value.unwrap();
1490 if let solana_account_decoder::UiAccountData::Json(parsed) = destination_account.data {
1491 let amount = parsed.parsed["info"]["tokenAmount"]["amount"]
1492 .as_str()
1493 .unwrap();
1494 assert_eq!(amount, "50");
1495 } else {
1496 panic!("destination account data was not in json parsed format");
1497 }
1498 }
1499}