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![];
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 svm_reader
477 .blocks
478 .contains_key(&block)
479 .map_err(SurfpoolError::from)
480 .map(|exists| (svm_reader.get_latest_absolute_slot(), exists))
481 })
482 .map_err(Into::<jsonrpc_core::Error>::into)??;
483
484 if !block_exists && block > current_slot {
486 return Err(jsonrpc_core::Error::invalid_params(format!(
487 "Block {} not found",
488 block
489 )));
490 }
491
492 let commitment_array = [0u64; 32];
493
494 Ok(RpcBlockCommitment {
495 commitment: Some(commitment_array),
496 total_stake: 0,
497 })
498 }
499
500 fn get_token_account_balance(
505 &self,
506 meta: Self::Metadata,
507 pubkey_str: String,
508 commitment: Option<CommitmentConfig>,
509 ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>> {
510 let pubkey = match verify_pubkey(&pubkey_str) {
511 Ok(res) => res,
512 Err(e) => return e.into(),
513 };
514
515 let SurfnetRpcContext {
516 svm_locker,
517 remote_ctx,
518 } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
519 Ok(res) => res,
520 Err(e) => return e.into(),
521 };
522
523 Box::pin(async move {
524 let token_account_result = svm_locker
525 .get_account(&remote_ctx, &pubkey, None)
526 .await?
527 .inner;
528
529 svm_locker.write_account_update(token_account_result.clone());
530
531 let token_account = token_account_result.map_account()?;
532
533 let (mint_pubkey, _amount) = if is_supported_token_program(&token_account.owner) {
534 let unpacked_token_account = TokenAccount::unpack(&token_account.data)?;
535 (
536 unpacked_token_account.mint(),
537 unpacked_token_account.amount(),
538 )
539 } else {
540 return Err(SurfpoolError::invalid_account_data(
541 pubkey,
542 "Account is not owned by Token or Token-2022 program",
543 None::<String>,
544 )
545 .into());
546 };
547
548 let SvmAccessContext {
549 slot,
550 inner: mint_account_result,
551 ..
552 } = svm_locker
553 .get_account(&remote_ctx, &mint_pubkey, None)
554 .await?;
555
556 svm_locker.write_account_update(mint_account_result.clone());
557
558 let mint_account = mint_account_result.map_account()?;
559
560 let token_decimals = if is_supported_token_program(&mint_account.owner) {
561 let unpacked_mint_account = MintAccount::unpack(&mint_account.data)?;
562 unpacked_mint_account.decimals()
563 } else {
564 return Err(SurfpoolError::invalid_account_data(
565 mint_pubkey,
566 "Mint account is not owned by Token or Token-2022 program",
567 None::<String>,
568 )
569 .into());
570 };
571
572 Ok(RpcResponse {
573 context: RpcResponseContext::new(slot),
574 value: {
575 parse_token_v3(
576 &token_account.data,
577 Some(&SplTokenAdditionalDataV2 {
578 decimals: token_decimals,
579 ..Default::default()
580 }),
581 )
582 .ok()
583 .and_then(|t| match t {
584 TokenAccountType::Account(account) => Some(account.token_amount),
585 _ => None,
586 })
587 },
588 })
589 })
590 }
591
592 fn get_token_supply(
593 &self,
594 meta: Self::Metadata,
595 mint_str: String,
596 commitment: Option<CommitmentConfig>,
597 ) -> BoxFuture<Result<RpcResponse<UiTokenAmount>>> {
598 let mint_pubkey = match verify_pubkey(&mint_str) {
599 Ok(pubkey) => pubkey,
600 Err(e) => return e.into(),
601 };
602
603 let SurfnetRpcContext {
604 svm_locker,
605 remote_ctx,
606 } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
607 Ok(res) => res,
608 Err(e) => return e.into(),
609 };
610
611 Box::pin(async move {
612 let SvmAccessContext {
613 slot,
614 inner: mint_account_result,
615 ..
616 } = svm_locker
617 .get_account(&remote_ctx, &mint_pubkey, None)
618 .await?;
619
620 svm_locker.write_account_update(mint_account_result.clone());
621
622 let mint_account = mint_account_result.map_account()?;
623
624 if !is_supported_token_program(&mint_account.owner) {
625 return Err(SurfpoolError::invalid_account_data(
626 mint_pubkey,
627 "Account is not a token mint account",
628 None::<String>,
629 )
630 .into());
631 }
632
633 let mint_data = MintAccount::unpack(&mint_account.data)?;
634
635 Ok(RpcResponse {
636 context: RpcResponseContext::new(slot),
637 value: {
638 parse_token_v3(
639 &mint_account.data,
640 Some(&SplTokenAdditionalDataV2 {
641 decimals: mint_data.decimals(),
642 ..Default::default()
643 }),
644 )
645 .ok()
646 .and_then(|t| match t {
647 TokenAccountType::Mint(mint) => {
648 let supply_u64 = mint.supply.parse::<u64>().unwrap_or(0);
649 let ui_amount = if supply_u64 == 0 {
650 Some(0.0)
651 } else {
652 let divisor = 10_u64.pow(mint.decimals as u32);
653 Some(supply_u64 as f64 / divisor as f64)
654 };
655
656 Some(UiTokenAmount {
657 amount: mint.supply.clone(),
658 decimals: mint.decimals,
659 ui_amount,
660 ui_amount_string: mint.supply,
661 })
662 }
663 _ => None,
664 })
665 .ok_or_else(|| {
666 SurfpoolError::invalid_account_data(
667 mint_pubkey,
668 "Failed to parse token mint account",
669 None::<String>,
670 )
671 })?
672 },
673 })
674 })
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use solana_account::Account;
681 use solana_keypair::Keypair;
682 use solana_program_option::COption;
683 use solana_program_pack::Pack;
684 use solana_pubkey::{Pubkey, new_rand};
685 use solana_signer::Signer;
686 use solana_system_interface::instruction::create_account;
687 use solana_transaction::Transaction;
688 use spl_associated_token_account_interface::{
689 address::get_associated_token_address_with_program_id,
690 instruction::create_associated_token_account,
691 };
692 use spl_token_2022_interface::instruction::{initialize_mint2, mint_to, transfer_checked};
693 use spl_token_interface::state::{Account as TokenAccount, AccountState, Mint};
694
695 use super::*;
696 use crate::{
697 surfnet::{GetAccountResult, remote::SurfnetRemoteClient},
698 tests::helpers::TestSetup,
699 types::SyntheticBlockhash,
700 };
701
702 #[ignore = "connection-required"]
703 #[tokio::test(flavor = "multi_thread")]
704 async fn test_get_token_account_balance() {
705 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
706
707 let mint_pk = Pubkey::new_unique();
708
709 let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
710 svm_reader
711 .inner
712 .minimum_balance_for_rent_exemption(Mint::LEN)
713 });
714
715 let mut data = [0; Mint::LEN];
716
717 let default = Mint {
718 decimals: 6,
719 supply: 1000000000000000,
720 is_initialized: true,
721 ..Default::default()
722 };
723 default.pack_into_slice(&mut data);
724
725 let mint_account = Account {
726 lamports: minimum_rent,
727 owner: spl_token_interface::ID,
728 executable: false,
729 rent_epoch: 0,
730 data: data.to_vec(),
731 };
732
733 setup
734 .context
735 .svm_locker
736 .write_account_update(GetAccountResult::FoundAccount(mint_pk, mint_account, true));
737
738 let token_account_pk = Pubkey::new_unique();
739
740 let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
741 svm_reader
742 .inner
743 .minimum_balance_for_rent_exemption(TokenAccount::LEN)
744 });
745
746 let mut data = [0; TokenAccount::LEN];
747
748 let default = TokenAccount {
749 mint: mint_pk,
750 owner: spl_token_interface::ID,
751 state: AccountState::Initialized,
752 amount: 100 * 1000000,
753 ..Default::default()
754 };
755 default.pack_into_slice(&mut data);
756
757 let token_account = Account {
758 lamports: minimum_rent,
759 owner: spl_token_interface::ID,
760 executable: false,
761 rent_epoch: 0,
762 data: data.to_vec(),
763 };
764
765 setup
766 .context
767 .svm_locker
768 .write_account_update(GetAccountResult::FoundAccount(
769 token_account_pk,
770 token_account,
771 true,
772 ));
773
774 let res = setup
775 .rpc
776 .get_token_account_balance(Some(setup.context), token_account_pk.to_string(), None)
777 .await
778 .unwrap();
779
780 assert_eq!(
781 res.value.unwrap(),
782 UiTokenAmount {
783 amount: String::from("100000000"),
784 decimals: 6,
785 ui_amount: Some(100.0),
786 ui_amount_string: String::from("100")
787 }
788 );
789 }
790
791 #[test]
792 fn test_get_block_commitment_past_slot() {
793 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
794 let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
795 let past_slot = if current_slot > 10 {
796 current_slot - 10
797 } else {
798 0
799 };
800
801 let result = setup
802 .rpc
803 .get_block_commitment(Some(setup.context), past_slot)
804 .unwrap();
805
806 assert!(result.commitment.is_some());
808 assert_eq!(result.total_stake, 0);
809 }
810
811 #[test]
812 fn test_get_block_commitment_with_actual_block() {
813 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
814
815 let test_slot = 12345;
817 setup.context.svm_locker.with_svm_writer(|svm_writer| {
818 use crate::surfnet::BlockHeader;
819
820 svm_writer
821 .blocks
822 .store(
823 test_slot,
824 BlockHeader {
825 hash: SyntheticBlockhash::new(test_slot).to_string(),
826 previous_blockhash: SyntheticBlockhash::new(test_slot - 1).to_string(),
827 parent_slot: test_slot - 1,
828 block_time: chrono::Utc::now().timestamp_millis(),
829 block_height: test_slot,
830 signatures: vec![],
831 },
832 )
833 .unwrap();
834 });
835
836 let result = setup
837 .rpc
838 .get_block_commitment(Some(setup.context), test_slot)
839 .unwrap();
840
841 assert!(result.commitment.is_some());
843 assert_eq!(result.total_stake, 0);
844 }
845
846 #[test]
847 fn test_get_block_commitment_no_metadata() {
848 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
849
850 let result = setup.rpc.get_block_commitment(None, 123);
851
852 assert!(result.is_err());
853 }
855
856 #[test]
857 fn test_get_block_commitment_future_slot_error() {
858 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
859 let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
860 let future_slot = current_slot + 1000;
861
862 let result = setup
863 .rpc
864 .get_block_commitment(Some(setup.context), future_slot);
865
866 assert!(result.is_err());
868
869 let error = result.unwrap_err();
870 assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
871 assert!(error.message.contains("Block") && error.message.contains("not found"));
872 }
873
874 #[tokio::test(flavor = "multi_thread")]
875 async fn test_get_token_supply_with_real_mint() {
876 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
877
878 let mint_pubkey = Pubkey::new_unique();
879
880 let mut mint_data = [0u8; Mint::LEN];
882 let mint = Mint {
883 mint_authority: COption::Some(Pubkey::new_unique()),
884 supply: 1_000_000_000_000,
885 decimals: 6,
886 is_initialized: true,
887 freeze_authority: COption::None,
888 };
889 Mint::pack(mint, &mut mint_data).unwrap();
890
891 let mint_account = Account {
892 lamports: setup.context.svm_locker.with_svm_reader(|svm_reader| {
893 svm_reader
894 .inner
895 .minimum_balance_for_rent_exemption(Mint::LEN)
896 }),
897 data: mint_data.to_vec(),
898 owner: spl_token_interface::id(),
899 executable: false,
900 rent_epoch: 0,
901 };
902
903 setup.context.svm_locker.with_svm_writer(|svm_writer| {
904 svm_writer
905 .set_account(&mint_pubkey, mint_account.clone())
906 .unwrap();
907 });
908
909 let res = setup
910 .rpc
911 .get_token_supply(
912 Some(setup.context),
913 mint_pubkey.to_string(),
914 Some(CommitmentConfig::confirmed()),
915 )
916 .await
917 .unwrap();
918
919 assert_eq!(res.value.amount, "1000000000000");
920 assert_eq!(res.value.decimals, 6);
921 assert_eq!(res.value.ui_amount_string, "1000000000000");
922 }
923
924 #[tokio::test(flavor = "multi_thread")]
925 async fn test_invalid_pubkey_format() {
926 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
927
928 let invalid_pubkeys = vec![
930 "",
931 "invalid",
932 "123",
933 "not-a-valid-base58-string!@#$",
934 "11111111111111111111111111111111111111111111111111111111111111111",
935 "invalid-base58-characters-ö",
936 ];
937
938 for invalid_pubkey in invalid_pubkeys {
939 let res = setup
940 .rpc
941 .get_token_supply(
942 Some(setup.context.clone()),
943 invalid_pubkey.to_string(),
944 Some(CommitmentConfig::confirmed()),
945 )
946 .await;
947
948 assert!(
949 res.is_err(),
950 "Should fail for invalid pubkey: '{}'",
951 invalid_pubkey
952 );
953
954 let error_msg = res.unwrap_err().to_string();
955 assert!(
956 error_msg.contains("Invalid") || error_msg.contains("invalid"),
957 "Error should mention invalidity for '{}': {}",
958 invalid_pubkey,
959 error_msg
960 );
961 }
962
963 println!("✅ All invalid pubkey formats correctly rejected");
964 }
965
966 #[tokio::test(flavor = "multi_thread")]
967 async fn test_nonexistent_account() {
968 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
969
970 let nonexistent_mint = Pubkey::new_unique();
972
973 let res = setup
974 .rpc
975 .get_token_supply(
976 Some(setup.context),
977 nonexistent_mint.to_string(),
978 Some(CommitmentConfig::confirmed()),
979 )
980 .await;
981
982 assert!(res.is_err(), "Should fail for nonexistent account");
983
984 let error_msg = res.unwrap_err().to_string();
985 assert!(
986 error_msg.contains("not found") || error_msg.contains("account"),
987 "Error should mention account not found: {}",
988 error_msg
989 );
990
991 println!("✅ Nonexistent account correctly rejected: {}", error_msg);
992 }
993
994 #[tokio::test(flavor = "multi_thread")]
995 async fn test_invalid_mint_data() {
996 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
997
998 let fake_mint = Pubkey::new_unique();
999
1000 setup.context.svm_locker.with_svm_writer(|svm_writer| {
1001 let invalid_mint_account = Account {
1003 lamports: 1000000,
1004 data: vec![0xFF; 50], owner: spl_token_interface::id(),
1006 executable: false,
1007 rent_epoch: 0,
1008 };
1009
1010 svm_writer
1011 .set_account(&fake_mint, invalid_mint_account)
1012 .unwrap();
1013 });
1014
1015 let res = setup
1016 .rpc
1017 .get_token_supply(
1018 Some(setup.context),
1019 fake_mint.to_string(),
1020 Some(CommitmentConfig::confirmed()),
1021 )
1022 .await;
1023
1024 assert!(
1025 res.is_err(),
1026 "Should fail for account with invalid mint data"
1027 );
1028
1029 let error_msg = res.unwrap_err().to_string();
1030 assert!(
1031 error_msg.eq("Parse error: Failed to unpack mint account"),
1032 "Incorrect error received: {}",
1033 error_msg
1034 );
1035
1036 println!("✅ Invalid mint data correctly rejected: {}", error_msg);
1037 }
1038
1039 #[ignore = "requires-network"]
1040 #[tokio::test(flavor = "multi_thread")]
1041 async fn test_remote_rpc_failure() {
1042 let bad_remote_client =
1044 SurfnetRemoteClient::new("https://invalid-url-that-doesnt-exist.com");
1045 let mut setup = TestSetup::new(SurfpoolAccountsDataRpc);
1046 setup.context.remote_rpc_client = Some(bad_remote_client);
1047
1048 let nonexistent_mint = Pubkey::new_unique();
1049
1050 let res = setup
1051 .rpc
1052 .get_token_supply(
1053 Some(setup.context),
1054 nonexistent_mint.to_string(),
1055 Some(CommitmentConfig::confirmed()),
1056 )
1057 .await;
1058
1059 assert!(res.is_err(), "Should fail when remote RPC is unreachable");
1060
1061 let error_msg = res.unwrap_err().to_string();
1062 println!("✅ Remote RPC failure handled: {}", error_msg);
1063 }
1064
1065 #[tokio::test(flavor = "multi_thread")]
1066 async fn test_transfer_token() {
1067 let client = TestSetup::new(SurfpoolAccountsDataRpc);
1069 let recent_blockhash = client
1070 .context
1071 .svm_locker
1072 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1073
1074 let fee_payer = Keypair::new();
1076
1077 let recipient = Keypair::new();
1079
1080 client
1082 .context
1083 .svm_locker
1084 .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1085 .unwrap()
1086 .unwrap();
1087
1088 client
1090 .context
1091 .svm_locker
1092 .airdrop(&recipient.pubkey(), 1_000_000_000)
1093 .unwrap()
1094 .unwrap();
1095
1096 let mint = Keypair::new();
1098
1099 let mint_space = Mint::LEN;
1101 let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1102 svm_reader
1103 .inner
1104 .minimum_balance_for_rent_exemption(mint_space)
1105 });
1106
1107 let create_account_instruction = create_account(
1109 &fee_payer.pubkey(), &mint.pubkey(), mint_rent, mint_space as u64, &spl_token_2022_interface::id(), );
1115
1116 let initialize_mint_instruction = initialize_mint2(
1118 &spl_token_2022_interface::id(),
1119 &mint.pubkey(), &fee_payer.pubkey(), Some(&fee_payer.pubkey()), 2, )
1124 .unwrap();
1125
1126 let source_token_address = get_associated_token_address_with_program_id(
1128 &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1132
1133 let create_source_ata_instruction = create_associated_token_account(
1135 &fee_payer.pubkey(), &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1140
1141 let destination_token_address = get_associated_token_address_with_program_id(
1143 &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1147
1148 let create_destination_ata_instruction = create_associated_token_account(
1150 &fee_payer.pubkey(), &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1155
1156 let amount = 100_00;
1158
1159 let mint_to_instruction = mint_to(
1161 &spl_token_2022_interface::id(),
1162 &mint.pubkey(), &source_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], amount, )
1168 .unwrap();
1169
1170 let transaction = Transaction::new_signed_with_payer(
1172 &[
1173 create_account_instruction,
1174 initialize_mint_instruction,
1175 create_source_ata_instruction,
1176 create_destination_ata_instruction,
1177 mint_to_instruction,
1178 ],
1179 Some(&fee_payer.pubkey()),
1180 &[&fee_payer, &mint],
1181 recent_blockhash,
1182 );
1183
1184 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1185 client
1186 .context
1187 .svm_locker
1188 .process_transaction(&None, transaction.into(), status_tx.clone(), false, true)
1189 .await
1190 .unwrap();
1191
1192 println!("Mint Address: {}", mint.pubkey());
1193 println!("Recipient Address: {}", recipient.pubkey());
1194 println!("Source Token Account Address: {}", source_token_address);
1195 println!(
1196 "Destination Token Account Address: {}",
1197 destination_token_address
1198 );
1199 println!("Minted {} tokens to the source token account", amount);
1200
1201 let recent_blockhash = client
1203 .context
1204 .svm_locker
1205 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1206
1207 let transfer_amount = 50;
1209
1210 let transfer_instruction = transfer_checked(
1212 &spl_token_2022_interface::id(), &source_token_address, &mint.pubkey(), &destination_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], transfer_amount, 2, )
1221 .unwrap();
1222
1223 let transaction = Transaction::new_signed_with_payer(
1225 &[transfer_instruction],
1226 Some(&fee_payer.pubkey()),
1227 &[&fee_payer],
1228 recent_blockhash,
1229 );
1230
1231 client
1232 .context
1233 .svm_locker
1234 .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1235 .await
1236 .unwrap();
1237
1238 println!("Successfully transferred 0.50 tokens from sender to recipient");
1239
1240 let source_balance = client
1241 .rpc
1242 .get_token_account_balance(
1243 Some(client.context.clone()),
1244 source_token_address.to_string(),
1245 Some(CommitmentConfig::confirmed()),
1246 )
1247 .await
1248 .unwrap();
1249
1250 let destination_balance = client
1251 .rpc
1252 .get_token_account_balance(
1253 Some(client.context.clone()),
1254 destination_token_address.to_string(),
1255 Some(CommitmentConfig::confirmed()),
1256 )
1257 .await
1258 .unwrap();
1259
1260 println!(
1261 "Source Token Account Balance: {} tokens ({})",
1262 source_balance.value.as_ref().unwrap().ui_amount.unwrap(),
1263 source_balance.value.as_ref().unwrap().amount
1264 );
1265 println!(
1266 "Destination Token Account Balance: {} tokens ({})",
1267 destination_balance
1268 .value
1269 .as_ref()
1270 .unwrap()
1271 .ui_amount
1272 .unwrap(),
1273 destination_balance.value.as_ref().unwrap().amount
1274 );
1275
1276 assert_eq!(source_balance.value.unwrap().amount, "9950");
1277 assert_eq!(destination_balance.value.unwrap().amount, "50");
1278 }
1279
1280 #[tokio::test(flavor = "multi_thread")]
1281 async fn test_get_account_info() {
1282 let client = TestSetup::new(SurfpoolAccountsDataRpc);
1284 let recent_blockhash = client
1285 .context
1286 .svm_locker
1287 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1288
1289 let fee_payer = Keypair::new();
1291
1292 let recipient = Keypair::new();
1294
1295 client
1297 .context
1298 .svm_locker
1299 .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1300 .unwrap()
1301 .unwrap();
1302
1303 client
1305 .context
1306 .svm_locker
1307 .airdrop(&recipient.pubkey(), 1_000_000_000)
1308 .unwrap()
1309 .unwrap();
1310
1311 let mint = Keypair::new();
1313
1314 let mint_space = Mint::LEN;
1316 let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1317 svm_reader
1318 .inner
1319 .minimum_balance_for_rent_exemption(mint_space)
1320 });
1321
1322 let create_account_instruction = create_account(
1324 &fee_payer.pubkey(), &mint.pubkey(), mint_rent, mint_space as u64, &spl_token_2022_interface::id(), );
1330
1331 let initialize_mint_instruction = initialize_mint2(
1333 &spl_token_2022_interface::id(),
1334 &mint.pubkey(), &fee_payer.pubkey(), Some(&fee_payer.pubkey()), 2, )
1339 .unwrap();
1340
1341 let source_token_address = get_associated_token_address_with_program_id(
1343 &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1347
1348 let create_source_ata_instruction = create_associated_token_account(
1350 &fee_payer.pubkey(), &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1355
1356 let destination_token_address = get_associated_token_address_with_program_id(
1358 &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1362
1363 let create_destination_ata_instruction = create_associated_token_account(
1365 &fee_payer.pubkey(), &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1370
1371 let amount = 100_00;
1373
1374 let mint_to_instruction = mint_to(
1376 &spl_token_2022_interface::id(),
1377 &mint.pubkey(), &source_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], amount, )
1383 .unwrap();
1384
1385 let transaction = Transaction::new_signed_with_payer(
1387 &[
1388 create_account_instruction,
1389 initialize_mint_instruction,
1390 create_source_ata_instruction,
1391 create_destination_ata_instruction,
1392 mint_to_instruction,
1393 ],
1394 Some(&fee_payer.pubkey()),
1395 &[&fee_payer, &mint],
1396 recent_blockhash,
1397 );
1398
1399 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1400 client
1402 .context
1403 .svm_locker
1404 .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1405 .await
1406 .unwrap();
1407
1408 println!("Mint Address: {}", mint.pubkey());
1409 println!("Recipient Address: {}", recipient.pubkey());
1410 println!("Source Token Account Address: {}", source_token_address);
1411 println!(
1412 "Destination Token Account Address: {}",
1413 destination_token_address
1414 );
1415 println!("Minted {} tokens to the source token account", amount);
1416
1417 let recent_blockhash = client
1419 .context
1420 .svm_locker
1421 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1422
1423 let transfer_amount = 50;
1425
1426 let transfer_instruction = transfer_checked(
1428 &spl_token_2022_interface::id(), &source_token_address, &mint.pubkey(), &destination_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], transfer_amount, 2, )
1437 .unwrap();
1438
1439 let transaction = Transaction::new_signed_with_payer(
1441 &[transfer_instruction],
1442 Some(&fee_payer.pubkey()),
1443 &[&fee_payer],
1444 recent_blockhash,
1445 );
1446 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1447 client
1449 .context
1450 .svm_locker
1451 .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1452 .await
1453 .unwrap();
1454
1455 println!(
1456 "Successfully transferred 0.50 tokens from {} to {}",
1457 source_token_address, destination_token_address
1458 );
1459
1460 let source_account_info = client
1461 .rpc
1462 .get_account_info(
1463 Some(client.context.clone()),
1464 source_token_address.to_string(),
1465 Some(RpcAccountInfoConfig {
1466 encoding: Some(solana_account_decoder::UiAccountEncoding::JsonParsed),
1467 ..Default::default()
1468 }),
1469 )
1470 .await
1471 .unwrap();
1472
1473 let destination_account_info = client
1474 .rpc
1475 .get_account_info(
1476 Some(client.context.clone()),
1477 destination_token_address.to_string(),
1478 Some(RpcAccountInfoConfig {
1479 encoding: Some(solana_account_decoder::UiAccountEncoding::JsonParsed),
1480 ..Default::default()
1481 }),
1482 )
1483 .await
1484 .unwrap();
1485
1486 println!("Source Account Info: {:?}", source_account_info);
1487 println!("Destination Account Info: {:?}", destination_account_info);
1488
1489 let source_account = source_account_info.value.unwrap();
1490 if let solana_account_decoder::UiAccountData::Json(parsed) = source_account.data {
1491 let amount = parsed.parsed["info"]["tokenAmount"]["amount"]
1492 .as_str()
1493 .unwrap();
1494 assert_eq!(amount, "9950");
1495 } else {
1496 panic!("source account data was not in json parsed format");
1497 }
1498
1499 let destination_account = destination_account_info.value.unwrap();
1500 if let solana_account_decoder::UiAccountData::Json(parsed) = destination_account.data {
1501 let amount = parsed.parsed["info"]["tokenAmount"]["amount"]
1502 .as_str()
1503 .unwrap();
1504 assert_eq!(amount, "50");
1505 } else {
1506 panic!("destination account data was not in json parsed format");
1507 }
1508 }
1509
1510 #[ignore = "requires-network"]
1511 #[tokio::test(flavor = "multi_thread")]
1512 async fn test_get_multiple_accounts_with_remote_preserves_order() {
1513 let mut setup = TestSetup::new(SurfpoolAccountsDataRpc);
1515
1516 let remote_client = SurfnetRemoteClient::new("https://api.mainnet-beta.solana.com");
1518 setup.context.remote_rpc_client = Some(remote_client);
1519
1520 let pk1 = new_rand();
1522 let pk2 = new_rand();
1523 let pk3 = new_rand();
1524
1525 println!("{}", pk1);
1526 println!("{}", pk2);
1527 println!("{}", pk3);
1528
1529 let account1 = Account {
1530 lamports: 1_000_000,
1531 data: vec![],
1532 owner: solana_pubkey::Pubkey::default(),
1533 executable: false,
1534 rent_epoch: 0,
1535 };
1536
1537 let account3 = Account {
1538 lamports: 3_000_000,
1539 data: vec![],
1540 owner: solana_pubkey::Pubkey::default(),
1541 executable: false,
1542 rent_epoch: 0,
1543 };
1544
1545 setup
1547 .context
1548 .svm_locker
1549 .write_account_update(GetAccountResult::FoundAccount(pk1, account1, true));
1550 setup
1551 .context
1552 .svm_locker
1553 .write_account_update(GetAccountResult::FoundAccount(pk3, account3, true));
1554
1555 let pubkeys_str = vec![pk1.to_string(), pk2.to_string(), pk3.to_string()];
1558
1559 let response = setup
1560 .rpc
1561 .get_multiple_accounts(
1562 Some(setup.context),
1563 pubkeys_str,
1564 Some(RpcAccountInfoConfig::default()),
1565 )
1566 .await
1567 .unwrap();
1568
1569 assert_eq!(response.value.len(), 3);
1571
1572 println!("{:?}", response);
1573
1574 assert!(response.value[0].is_some());
1576 assert_eq!(
1577 response.value[0].as_ref().unwrap().lamports,
1578 1_000_000,
1579 "First element should be account1"
1580 );
1581
1582 assert!(
1584 response.value[1].is_none(),
1585 "Second element should be None for missing pk2"
1586 );
1587
1588 assert!(response.value[2].is_some());
1590 assert_eq!(
1591 response.value[2].as_ref().unwrap().lamports,
1592 3_000_000,
1593 "Third element should be account3"
1594 );
1595
1596 println!("✅ Account order preserved with remote: [1M lamports, None, 3M lamports]");
1597 }
1598}