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 #[cfg(feature = "prometheus")]
365 let rpc_start = std::time::Instant::now();
366
367 let SurfnetRpcContext {
368 svm_locker,
369 remote_ctx,
370 } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
371 Ok(res) => res,
372 Err(e) => return e.into(),
373 };
374
375 Box::pin(async move {
376 let SvmAccessContext {
377 slot,
378 inner: account_update,
379 ..
380 } = svm_locker.get_account(&remote_ctx, &pubkey, None).await?;
381
382 #[cfg(feature = "prometheus")]
383 if let Some(m) = crate::telemetry::metrics() {
384 m.record_rpc_request("getAccountInfo", rpc_start.elapsed().as_millis() as u64);
385 }
386 svm_locker.write_account_update(account_update.clone());
387
388 let ui_account = if let Some(((pubkey, account), token_data)) =
389 account_update.map_account_with_token_data()
390 {
391 Some(
392 svm_locker
393 .account_to_rpc_keyed_account(
394 &pubkey,
395 &account,
396 &config,
397 token_data.map(|(mint, _)| mint),
398 )
399 .account,
400 )
401 } else {
402 None
403 };
404
405 Ok(RpcResponse {
406 context: RpcResponseContext::new(slot),
407 value: ui_account,
408 })
409 })
410 }
411
412 fn get_multiple_accounts(
413 &self,
414 meta: Self::Metadata,
415 pubkeys_str: Vec<String>,
416 config: Option<RpcAccountInfoConfig>,
417 ) -> BoxFuture<Result<RpcResponse<Vec<Option<UiAccount>>>>> {
418 let config = config.unwrap_or_default();
419 let pubkeys = match pubkeys_str
420 .iter()
421 .map(|s| verify_pubkey(s))
422 .collect::<SurfpoolResult<Vec<_>>>()
423 {
424 Ok(p) => p,
425 Err(e) => return e.into(),
426 };
427
428 let SurfnetRpcContext {
429 svm_locker,
430 remote_ctx,
431 } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
432 Ok(res) => res,
433 Err(e) => return e.into(),
434 };
435
436 #[cfg(feature = "prometheus")]
437 let rpc_start = std::time::Instant::now();
438
439 Box::pin(async move {
440 let SvmAccessContext {
441 slot,
442 inner: account_updates,
443 ..
444 } = svm_locker
445 .get_multiple_accounts(&remote_ctx, &pubkeys, None)
446 .await?;
447
448 #[cfg(feature = "prometheus")]
449 if let Some(m) = crate::telemetry::metrics() {
450 m.record_rpc_request(
451 "getMultipleAccounts",
452 rpc_start.elapsed().as_millis() as u64,
453 );
454 }
455
456 svm_locker.write_multiple_account_updates(&account_updates);
457
458 let mut ui_accounts = vec![];
460 for account_update in account_updates.into_iter() {
461 if let Some(((pubkey, account), token_data)) =
462 account_update.map_account_with_token_data()
463 {
464 ui_accounts.push(Some(
465 svm_locker
466 .account_to_rpc_keyed_account(
467 &pubkey,
468 &account,
469 &config,
470 token_data.map(|(mint, _)| mint),
471 )
472 .account,
473 ));
474 } else {
475 ui_accounts.push(None);
476 }
477 }
478
479 Ok(RpcResponse {
480 context: RpcResponseContext::new(slot),
481 value: ui_accounts,
482 })
483 })
484 }
485
486 fn get_block_commitment(
487 &self,
488 meta: Self::Metadata,
489 block: Slot,
490 ) -> Result<RpcBlockCommitment<BlockCommitmentArray>> {
491 let (current_slot, block_exists) = meta
493 .with_svm_reader(|svm_reader| {
494 svm_reader
495 .blocks
496 .contains_key(&block)
497 .map_err(SurfpoolError::from)
498 .map(|exists| (svm_reader.get_latest_absolute_slot(), exists))
499 })
500 .map_err(Into::<jsonrpc_core::Error>::into)??;
501
502 if !block_exists && block > current_slot {
504 return Err(jsonrpc_core::Error::invalid_params(format!(
505 "Block {} not found",
506 block
507 )));
508 }
509
510 let commitment_array = [0u64; 32];
511
512 Ok(RpcBlockCommitment {
513 commitment: Some(commitment_array),
514 total_stake: 0,
515 })
516 }
517
518 fn get_token_account_balance(
523 &self,
524 meta: Self::Metadata,
525 pubkey_str: String,
526 commitment: Option<CommitmentConfig>,
527 ) -> BoxFuture<Result<RpcResponse<Option<UiTokenAmount>>>> {
528 let pubkey = match verify_pubkey(&pubkey_str) {
529 Ok(res) => res,
530 Err(e) => return e.into(),
531 };
532
533 let SurfnetRpcContext {
534 svm_locker,
535 remote_ctx,
536 } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
537 Ok(res) => res,
538 Err(e) => return e.into(),
539 };
540
541 Box::pin(async move {
542 let token_account_result = svm_locker
543 .get_account(&remote_ctx, &pubkey, None)
544 .await?
545 .inner;
546
547 svm_locker.write_account_update(token_account_result.clone());
548
549 let token_account = token_account_result.map_account()?;
550
551 let (mint_pubkey, _amount) = if is_supported_token_program(&token_account.owner) {
552 let unpacked_token_account = TokenAccount::unpack(&token_account.data)?;
553 (
554 unpacked_token_account.mint(),
555 unpacked_token_account.amount(),
556 )
557 } else {
558 return Err(SurfpoolError::invalid_account_data(
559 pubkey,
560 "Account is not owned by Token or Token-2022 program",
561 None::<String>,
562 )
563 .into());
564 };
565
566 let SvmAccessContext {
567 slot,
568 inner: mint_account_result,
569 ..
570 } = svm_locker
571 .get_account(&remote_ctx, &mint_pubkey, None)
572 .await?;
573
574 svm_locker.write_account_update(mint_account_result.clone());
575
576 let mint_account = mint_account_result.map_account()?;
577
578 let token_decimals = if is_supported_token_program(&mint_account.owner) {
579 let unpacked_mint_account = MintAccount::unpack(&mint_account.data)?;
580 unpacked_mint_account.decimals()
581 } else {
582 return Err(SurfpoolError::invalid_account_data(
583 mint_pubkey,
584 "Mint account is not owned by Token or Token-2022 program",
585 None::<String>,
586 )
587 .into());
588 };
589
590 Ok(RpcResponse {
591 context: RpcResponseContext::new(slot),
592 value: {
593 parse_token_v3(
594 &token_account.data,
595 Some(&SplTokenAdditionalDataV2 {
596 decimals: token_decimals,
597 ..Default::default()
598 }),
599 )
600 .ok()
601 .and_then(|t| match t {
602 TokenAccountType::Account(account) => Some(account.token_amount),
603 _ => None,
604 })
605 },
606 })
607 })
608 }
609
610 fn get_token_supply(
611 &self,
612 meta: Self::Metadata,
613 mint_str: String,
614 commitment: Option<CommitmentConfig>,
615 ) -> BoxFuture<Result<RpcResponse<UiTokenAmount>>> {
616 let mint_pubkey = match verify_pubkey(&mint_str) {
617 Ok(pubkey) => pubkey,
618 Err(e) => return e.into(),
619 };
620
621 let SurfnetRpcContext {
622 svm_locker,
623 remote_ctx,
624 } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
625 Ok(res) => res,
626 Err(e) => return e.into(),
627 };
628
629 Box::pin(async move {
630 let SvmAccessContext {
631 slot,
632 inner: mint_account_result,
633 ..
634 } = svm_locker
635 .get_account(&remote_ctx, &mint_pubkey, None)
636 .await?;
637
638 svm_locker.write_account_update(mint_account_result.clone());
639
640 let mint_account = mint_account_result.map_account()?;
641
642 if !is_supported_token_program(&mint_account.owner) {
643 return Err(SurfpoolError::invalid_account_data(
644 mint_pubkey,
645 "Account is not a token mint account",
646 None::<String>,
647 )
648 .into());
649 }
650
651 let mint_data = MintAccount::unpack(&mint_account.data)?;
652
653 Ok(RpcResponse {
654 context: RpcResponseContext::new(slot),
655 value: {
656 parse_token_v3(
657 &mint_account.data,
658 Some(&SplTokenAdditionalDataV2 {
659 decimals: mint_data.decimals(),
660 ..Default::default()
661 }),
662 )
663 .ok()
664 .and_then(|t| match t {
665 TokenAccountType::Mint(mint) => {
666 let supply_u64 = mint.supply.parse::<u64>().unwrap_or(0);
667 let ui_amount = if supply_u64 == 0 {
668 Some(0.0)
669 } else {
670 let divisor = 10_u64.pow(mint.decimals as u32);
671 Some(supply_u64 as f64 / divisor as f64)
672 };
673
674 Some(UiTokenAmount {
675 amount: mint.supply.clone(),
676 decimals: mint.decimals,
677 ui_amount,
678 ui_amount_string: mint.supply,
679 })
680 }
681 _ => None,
682 })
683 .ok_or_else(|| {
684 SurfpoolError::invalid_account_data(
685 mint_pubkey,
686 "Failed to parse token mint account",
687 None::<String>,
688 )
689 })?
690 },
691 })
692 })
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use solana_account::Account;
699 use solana_keypair::Keypair;
700 use solana_program_option::COption;
701 use solana_program_pack::Pack;
702 use solana_pubkey::Pubkey;
703 use solana_signer::Signer;
704 use solana_system_interface::instruction::create_account;
705 use solana_transaction::Transaction;
706 use spl_associated_token_account_interface::{
707 address::get_associated_token_address_with_program_id,
708 instruction::create_associated_token_account,
709 };
710 use spl_token_2022_interface::instruction::{initialize_mint2, mint_to, transfer_checked};
711 use spl_token_interface::state::{Account as TokenAccount, AccountState, Mint};
712
713 use super::*;
714 use crate::{
715 surfnet::{GetAccountResult, remote::SurfnetRemoteClient},
716 tests::helpers::TestSetup,
717 types::SyntheticBlockhash,
718 };
719
720 #[ignore = "connection-required"]
721 #[tokio::test(flavor = "multi_thread")]
722 async fn test_get_token_account_balance() {
723 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
724
725 let mint_pk = Pubkey::new_unique();
726
727 let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
728 svm_reader
729 .inner
730 .minimum_balance_for_rent_exemption(Mint::LEN)
731 });
732
733 let mut data = [0; Mint::LEN];
734
735 let default = Mint {
736 decimals: 6,
737 supply: 1000000000000000,
738 is_initialized: true,
739 ..Default::default()
740 };
741 default.pack_into_slice(&mut data);
742
743 let mint_account = Account {
744 lamports: minimum_rent,
745 owner: spl_token_interface::ID,
746 executable: false,
747 rent_epoch: 0,
748 data: data.to_vec(),
749 };
750
751 setup
752 .context
753 .svm_locker
754 .write_account_update(GetAccountResult::FoundAccount(mint_pk, mint_account, true));
755
756 let token_account_pk = Pubkey::new_unique();
757
758 let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
759 svm_reader
760 .inner
761 .minimum_balance_for_rent_exemption(TokenAccount::LEN)
762 });
763
764 let mut data = [0; TokenAccount::LEN];
765
766 let default = TokenAccount {
767 mint: mint_pk,
768 owner: spl_token_interface::ID,
769 state: AccountState::Initialized,
770 amount: 100 * 1000000,
771 ..Default::default()
772 };
773 default.pack_into_slice(&mut data);
774
775 let token_account = Account {
776 lamports: minimum_rent,
777 owner: spl_token_interface::ID,
778 executable: false,
779 rent_epoch: 0,
780 data: data.to_vec(),
781 };
782
783 setup
784 .context
785 .svm_locker
786 .write_account_update(GetAccountResult::FoundAccount(
787 token_account_pk,
788 token_account,
789 true,
790 ));
791
792 let res = setup
793 .rpc
794 .get_token_account_balance(Some(setup.context), token_account_pk.to_string(), None)
795 .await
796 .unwrap();
797
798 assert_eq!(
799 res.value.unwrap(),
800 UiTokenAmount {
801 amount: String::from("100000000"),
802 decimals: 6,
803 ui_amount: Some(100.0),
804 ui_amount_string: String::from("100")
805 }
806 );
807 }
808
809 #[test]
810 fn test_get_block_commitment_past_slot() {
811 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
812 let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
813 let past_slot = if current_slot > 10 {
814 current_slot - 10
815 } else {
816 0
817 };
818
819 let result = setup
820 .rpc
821 .get_block_commitment(Some(setup.context), past_slot)
822 .unwrap();
823
824 assert!(result.commitment.is_some());
826 assert_eq!(result.total_stake, 0);
827 }
828
829 #[test]
830 fn test_get_block_commitment_with_actual_block() {
831 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
832
833 let test_slot = 12345;
835 setup.context.svm_locker.with_svm_writer(|svm_writer| {
836 use crate::surfnet::BlockHeader;
837
838 svm_writer
839 .blocks
840 .store(
841 test_slot,
842 BlockHeader {
843 hash: SyntheticBlockhash::new(test_slot).to_string(),
844 previous_blockhash: SyntheticBlockhash::new(test_slot - 1).to_string(),
845 parent_slot: test_slot - 1,
846 block_time: chrono::Utc::now().timestamp_millis(),
847 block_height: test_slot,
848 signatures: vec![],
849 },
850 )
851 .unwrap();
852 });
853
854 let result = setup
855 .rpc
856 .get_block_commitment(Some(setup.context), test_slot)
857 .unwrap();
858
859 assert!(result.commitment.is_some());
861 assert_eq!(result.total_stake, 0);
862 }
863
864 #[test]
865 fn test_get_block_commitment_no_metadata() {
866 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
867
868 let result = setup.rpc.get_block_commitment(None, 123);
869
870 assert!(result.is_err());
871 }
873
874 #[test]
875 fn test_get_block_commitment_future_slot_error() {
876 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
877 let current_slot = setup.context.svm_locker.get_latest_absolute_slot();
878 let future_slot = current_slot + 1000;
879
880 let result = setup
881 .rpc
882 .get_block_commitment(Some(setup.context), future_slot);
883
884 assert!(result.is_err());
886
887 let error = result.unwrap_err();
888 assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
889 assert!(error.message.contains("Block") && error.message.contains("not found"));
890 }
891
892 #[tokio::test(flavor = "multi_thread")]
893 async fn test_get_token_supply_with_real_mint() {
894 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
895
896 let mint_pubkey = Pubkey::new_unique();
897
898 let mut mint_data = [0u8; Mint::LEN];
900 let mint = Mint {
901 mint_authority: COption::Some(Pubkey::new_unique()),
902 supply: 1_000_000_000_000,
903 decimals: 6,
904 is_initialized: true,
905 freeze_authority: COption::None,
906 };
907 Mint::pack(mint, &mut mint_data).unwrap();
908
909 let mint_account = Account {
910 lamports: setup.context.svm_locker.with_svm_reader(|svm_reader| {
911 svm_reader
912 .inner
913 .minimum_balance_for_rent_exemption(Mint::LEN)
914 }),
915 data: mint_data.to_vec(),
916 owner: spl_token_interface::id(),
917 executable: false,
918 rent_epoch: 0,
919 };
920
921 setup.context.svm_locker.with_svm_writer(|svm_writer| {
922 svm_writer
923 .set_account(&mint_pubkey, mint_account.clone())
924 .unwrap();
925 });
926
927 let res = setup
928 .rpc
929 .get_token_supply(
930 Some(setup.context),
931 mint_pubkey.to_string(),
932 Some(CommitmentConfig::confirmed()),
933 )
934 .await
935 .unwrap();
936
937 assert_eq!(res.value.amount, "1000000000000");
938 assert_eq!(res.value.decimals, 6);
939 assert_eq!(res.value.ui_amount_string, "1000000000000");
940 }
941
942 #[tokio::test(flavor = "multi_thread")]
943 async fn test_invalid_pubkey_format() {
944 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
945
946 let invalid_pubkeys = vec![
948 "",
949 "invalid",
950 "123",
951 "not-a-valid-base58-string!@#$",
952 "11111111111111111111111111111111111111111111111111111111111111111",
953 "invalid-base58-characters-ö",
954 ];
955
956 for invalid_pubkey in invalid_pubkeys {
957 let res = setup
958 .rpc
959 .get_token_supply(
960 Some(setup.context.clone()),
961 invalid_pubkey.to_string(),
962 Some(CommitmentConfig::confirmed()),
963 )
964 .await;
965
966 assert!(
967 res.is_err(),
968 "Should fail for invalid pubkey: '{}'",
969 invalid_pubkey
970 );
971
972 let error_msg = res.unwrap_err().to_string();
973 assert!(
974 error_msg.contains("Invalid") || error_msg.contains("invalid"),
975 "Error should mention invalidity for '{}': {}",
976 invalid_pubkey,
977 error_msg
978 );
979 }
980
981 println!("✅ All invalid pubkey formats correctly rejected");
982 }
983
984 #[tokio::test(flavor = "multi_thread")]
985 async fn test_nonexistent_account() {
986 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
987
988 let nonexistent_mint = Pubkey::new_unique();
990
991 let res = setup
992 .rpc
993 .get_token_supply(
994 Some(setup.context),
995 nonexistent_mint.to_string(),
996 Some(CommitmentConfig::confirmed()),
997 )
998 .await;
999
1000 assert!(res.is_err(), "Should fail for nonexistent account");
1001
1002 let error_msg = res.unwrap_err().to_string();
1003 assert!(
1004 error_msg.contains("not found") || error_msg.contains("account"),
1005 "Error should mention account not found: {}",
1006 error_msg
1007 );
1008
1009 println!("✅ Nonexistent account correctly rejected: {}", error_msg);
1010 }
1011
1012 #[tokio::test(flavor = "multi_thread")]
1013 async fn test_invalid_mint_data() {
1014 let setup = TestSetup::new(SurfpoolAccountsDataRpc);
1015
1016 let fake_mint = Pubkey::new_unique();
1017
1018 setup.context.svm_locker.with_svm_writer(|svm_writer| {
1019 let invalid_mint_account = Account {
1021 lamports: 1000000,
1022 data: vec![0xFF; 50], owner: spl_token_interface::id(),
1024 executable: false,
1025 rent_epoch: 0,
1026 };
1027
1028 svm_writer
1029 .set_account(&fake_mint, invalid_mint_account)
1030 .unwrap();
1031 });
1032
1033 let res = setup
1034 .rpc
1035 .get_token_supply(
1036 Some(setup.context),
1037 fake_mint.to_string(),
1038 Some(CommitmentConfig::confirmed()),
1039 )
1040 .await;
1041
1042 assert!(
1043 res.is_err(),
1044 "Should fail for account with invalid mint data"
1045 );
1046
1047 let error_msg = res.unwrap_err().to_string();
1048 assert!(
1049 error_msg.eq("Parse error: Failed to unpack mint account"),
1050 "Incorrect error received: {}",
1051 error_msg
1052 );
1053
1054 println!("✅ Invalid mint data correctly rejected: {}", error_msg);
1055 }
1056
1057 #[ignore = "requires-network"]
1058 #[tokio::test(flavor = "multi_thread")]
1059 async fn test_remote_rpc_failure() {
1060 let bad_remote_client =
1062 SurfnetRemoteClient::new("https://invalid-url-that-doesnt-exist.com");
1063 let mut setup = TestSetup::new(SurfpoolAccountsDataRpc);
1064 setup.context.remote_rpc_client = Some(bad_remote_client);
1065
1066 let nonexistent_mint = Pubkey::new_unique();
1067
1068 let res = setup
1069 .rpc
1070 .get_token_supply(
1071 Some(setup.context),
1072 nonexistent_mint.to_string(),
1073 Some(CommitmentConfig::confirmed()),
1074 )
1075 .await;
1076
1077 assert!(res.is_err(), "Should fail when remote RPC is unreachable");
1078
1079 let error_msg = res.unwrap_err().to_string();
1080 println!("✅ Remote RPC failure handled: {}", error_msg);
1081 }
1082
1083 #[tokio::test(flavor = "multi_thread")]
1084 async fn test_transfer_token() {
1085 let client = TestSetup::new(SurfpoolAccountsDataRpc);
1087 let recent_blockhash = client
1088 .context
1089 .svm_locker
1090 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1091
1092 let fee_payer = Keypair::new();
1094
1095 let recipient = Keypair::new();
1097
1098 client
1100 .context
1101 .svm_locker
1102 .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1103 .unwrap()
1104 .unwrap();
1105
1106 client
1108 .context
1109 .svm_locker
1110 .airdrop(&recipient.pubkey(), 1_000_000_000)
1111 .unwrap()
1112 .unwrap();
1113
1114 let mint = Keypair::new();
1116
1117 let mint_space = Mint::LEN;
1119 let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1120 svm_reader
1121 .inner
1122 .minimum_balance_for_rent_exemption(mint_space)
1123 });
1124
1125 let create_account_instruction = create_account(
1127 &fee_payer.pubkey(), &mint.pubkey(), mint_rent, mint_space as u64, &spl_token_2022_interface::id(), );
1133
1134 let initialize_mint_instruction = initialize_mint2(
1136 &spl_token_2022_interface::id(),
1137 &mint.pubkey(), &fee_payer.pubkey(), Some(&fee_payer.pubkey()), 2, )
1142 .unwrap();
1143
1144 let source_token_address = get_associated_token_address_with_program_id(
1146 &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1150
1151 let create_source_ata_instruction = create_associated_token_account(
1153 &fee_payer.pubkey(), &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1158
1159 let destination_token_address = get_associated_token_address_with_program_id(
1161 &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1165
1166 let create_destination_ata_instruction = create_associated_token_account(
1168 &fee_payer.pubkey(), &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1173
1174 let amount = 100_00;
1176
1177 let mint_to_instruction = mint_to(
1179 &spl_token_2022_interface::id(),
1180 &mint.pubkey(), &source_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], amount, )
1186 .unwrap();
1187
1188 let transaction = Transaction::new_signed_with_payer(
1190 &[
1191 create_account_instruction,
1192 initialize_mint_instruction,
1193 create_source_ata_instruction,
1194 create_destination_ata_instruction,
1195 mint_to_instruction,
1196 ],
1197 Some(&fee_payer.pubkey()),
1198 &[&fee_payer, &mint],
1199 recent_blockhash,
1200 );
1201
1202 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1203 client
1204 .context
1205 .svm_locker
1206 .process_transaction(&None, transaction.into(), status_tx.clone(), false, true)
1207 .await
1208 .unwrap();
1209
1210 println!("Mint Address: {}", mint.pubkey());
1211 println!("Recipient Address: {}", recipient.pubkey());
1212 println!("Source Token Account Address: {}", source_token_address);
1213 println!(
1214 "Destination Token Account Address: {}",
1215 destination_token_address
1216 );
1217 println!("Minted {} tokens to the source token account", amount);
1218
1219 let recent_blockhash = client
1221 .context
1222 .svm_locker
1223 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1224
1225 let transfer_amount = 50;
1227
1228 let transfer_instruction = transfer_checked(
1230 &spl_token_2022_interface::id(), &source_token_address, &mint.pubkey(), &destination_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], transfer_amount, 2, )
1239 .unwrap();
1240
1241 let transaction = Transaction::new_signed_with_payer(
1243 &[transfer_instruction],
1244 Some(&fee_payer.pubkey()),
1245 &[&fee_payer],
1246 recent_blockhash,
1247 );
1248
1249 client
1250 .context
1251 .svm_locker
1252 .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1253 .await
1254 .unwrap();
1255
1256 println!("Successfully transferred 0.50 tokens from sender to recipient");
1257
1258 let source_balance = client
1259 .rpc
1260 .get_token_account_balance(
1261 Some(client.context.clone()),
1262 source_token_address.to_string(),
1263 Some(CommitmentConfig::confirmed()),
1264 )
1265 .await
1266 .unwrap();
1267
1268 let destination_balance = client
1269 .rpc
1270 .get_token_account_balance(
1271 Some(client.context.clone()),
1272 destination_token_address.to_string(),
1273 Some(CommitmentConfig::confirmed()),
1274 )
1275 .await
1276 .unwrap();
1277
1278 println!(
1279 "Source Token Account Balance: {} tokens ({})",
1280 source_balance.value.as_ref().unwrap().ui_amount.unwrap(),
1281 source_balance.value.as_ref().unwrap().amount
1282 );
1283 println!(
1284 "Destination Token Account Balance: {} tokens ({})",
1285 destination_balance
1286 .value
1287 .as_ref()
1288 .unwrap()
1289 .ui_amount
1290 .unwrap(),
1291 destination_balance.value.as_ref().unwrap().amount
1292 );
1293
1294 assert_eq!(source_balance.value.unwrap().amount, "9950");
1295 assert_eq!(destination_balance.value.unwrap().amount, "50");
1296 }
1297
1298 #[tokio::test(flavor = "multi_thread")]
1299 async fn test_get_account_info() {
1300 let client = TestSetup::new(SurfpoolAccountsDataRpc);
1302 let recent_blockhash = client
1303 .context
1304 .svm_locker
1305 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1306
1307 let fee_payer = Keypair::new();
1309
1310 let recipient = Keypair::new();
1312
1313 client
1315 .context
1316 .svm_locker
1317 .airdrop(&fee_payer.pubkey(), 1_000_000_000)
1318 .unwrap()
1319 .unwrap();
1320
1321 client
1323 .context
1324 .svm_locker
1325 .airdrop(&recipient.pubkey(), 1_000_000_000)
1326 .unwrap()
1327 .unwrap();
1328
1329 let mint = Keypair::new();
1331
1332 let mint_space = Mint::LEN;
1334 let mint_rent = client.context.svm_locker.with_svm_reader(|svm_reader| {
1335 svm_reader
1336 .inner
1337 .minimum_balance_for_rent_exemption(mint_space)
1338 });
1339
1340 let create_account_instruction = create_account(
1342 &fee_payer.pubkey(), &mint.pubkey(), mint_rent, mint_space as u64, &spl_token_2022_interface::id(), );
1348
1349 let initialize_mint_instruction = initialize_mint2(
1351 &spl_token_2022_interface::id(),
1352 &mint.pubkey(), &fee_payer.pubkey(), Some(&fee_payer.pubkey()), 2, )
1357 .unwrap();
1358
1359 let source_token_address = get_associated_token_address_with_program_id(
1361 &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1365
1366 let create_source_ata_instruction = create_associated_token_account(
1368 &fee_payer.pubkey(), &fee_payer.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1373
1374 let destination_token_address = get_associated_token_address_with_program_id(
1376 &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1380
1381 let create_destination_ata_instruction = create_associated_token_account(
1383 &fee_payer.pubkey(), &recipient.pubkey(), &mint.pubkey(), &spl_token_2022_interface::id(), );
1388
1389 let amount = 100_00;
1391
1392 let mint_to_instruction = mint_to(
1394 &spl_token_2022_interface::id(),
1395 &mint.pubkey(), &source_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], amount, )
1401 .unwrap();
1402
1403 let transaction = Transaction::new_signed_with_payer(
1405 &[
1406 create_account_instruction,
1407 initialize_mint_instruction,
1408 create_source_ata_instruction,
1409 create_destination_ata_instruction,
1410 mint_to_instruction,
1411 ],
1412 Some(&fee_payer.pubkey()),
1413 &[&fee_payer, &mint],
1414 recent_blockhash,
1415 );
1416
1417 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1418 client
1420 .context
1421 .svm_locker
1422 .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1423 .await
1424 .unwrap();
1425
1426 println!("Mint Address: {}", mint.pubkey());
1427 println!("Recipient Address: {}", recipient.pubkey());
1428 println!("Source Token Account Address: {}", source_token_address);
1429 println!(
1430 "Destination Token Account Address: {}",
1431 destination_token_address
1432 );
1433 println!("Minted {} tokens to the source token account", amount);
1434
1435 let recent_blockhash = client
1437 .context
1438 .svm_locker
1439 .with_svm_reader(|svm_reader| svm_reader.latest_blockhash());
1440
1441 let transfer_amount = 50;
1443
1444 let transfer_instruction = transfer_checked(
1446 &spl_token_2022_interface::id(), &source_token_address, &mint.pubkey(), &destination_token_address, &fee_payer.pubkey(), &[&fee_payer.pubkey()], transfer_amount, 2, )
1455 .unwrap();
1456
1457 let transaction = Transaction::new_signed_with_payer(
1459 &[transfer_instruction],
1460 Some(&fee_payer.pubkey()),
1461 &[&fee_payer],
1462 recent_blockhash,
1463 );
1464 let (status_tx, _status_rx) = crossbeam_channel::unbounded();
1465 client
1467 .context
1468 .svm_locker
1469 .process_transaction(&None, transaction.clone().into(), status_tx, true, true)
1470 .await
1471 .unwrap();
1472
1473 println!(
1474 "Successfully transferred 0.50 tokens from {} to {}",
1475 source_token_address, destination_token_address
1476 );
1477
1478 let source_account_info = client
1479 .rpc
1480 .get_account_info(
1481 Some(client.context.clone()),
1482 source_token_address.to_string(),
1483 Some(RpcAccountInfoConfig {
1484 encoding: Some(solana_account_decoder::UiAccountEncoding::JsonParsed),
1485 ..Default::default()
1486 }),
1487 )
1488 .await
1489 .unwrap();
1490
1491 let destination_account_info = client
1492 .rpc
1493 .get_account_info(
1494 Some(client.context.clone()),
1495 destination_token_address.to_string(),
1496 Some(RpcAccountInfoConfig {
1497 encoding: Some(solana_account_decoder::UiAccountEncoding::JsonParsed),
1498 ..Default::default()
1499 }),
1500 )
1501 .await
1502 .unwrap();
1503
1504 println!("Source Account Info: {:?}", source_account_info);
1505 println!("Destination Account Info: {:?}", destination_account_info);
1506
1507 let source_account = source_account_info.value.unwrap();
1508 if let solana_account_decoder::UiAccountData::Json(parsed) = source_account.data {
1509 let amount = parsed.parsed["info"]["tokenAmount"]["amount"]
1510 .as_str()
1511 .unwrap();
1512 assert_eq!(amount, "9950");
1513 } else {
1514 panic!("source account data was not in json parsed format");
1515 }
1516
1517 let destination_account = destination_account_info.value.unwrap();
1518 if let solana_account_decoder::UiAccountData::Json(parsed) = destination_account.data {
1519 let amount = parsed.parsed["info"]["tokenAmount"]["amount"]
1520 .as_str()
1521 .unwrap();
1522 assert_eq!(amount, "50");
1523 } else {
1524 panic!("destination account data was not in json parsed format");
1525 }
1526 }
1527
1528 #[ignore = "requires-network"]
1529 #[tokio::test(flavor = "multi_thread")]
1530 async fn test_get_multiple_accounts_with_remote_preserves_order() {
1531 let mut setup = TestSetup::new(SurfpoolAccountsDataRpc);
1533
1534 let remote_client = SurfnetRemoteClient::new("https://api.mainnet-beta.solana.com");
1536 setup.context.remote_rpc_client = Some(remote_client);
1537
1538 let pk1 = Pubkey::new_unique();
1540 let pk2 = Pubkey::new_unique();
1541 let pk3 = Pubkey::new_unique();
1542
1543 println!("{}", pk1);
1544 println!("{}", pk2);
1545 println!("{}", pk3);
1546
1547 let account1 = Account {
1548 lamports: 1_000_000,
1549 data: vec![],
1550 owner: solana_pubkey::Pubkey::default(),
1551 executable: false,
1552 rent_epoch: 0,
1553 };
1554
1555 let account3 = Account {
1556 lamports: 3_000_000,
1557 data: vec![],
1558 owner: solana_pubkey::Pubkey::default(),
1559 executable: false,
1560 rent_epoch: 0,
1561 };
1562
1563 setup
1565 .context
1566 .svm_locker
1567 .write_account_update(GetAccountResult::FoundAccount(pk1, account1, true));
1568 setup
1569 .context
1570 .svm_locker
1571 .write_account_update(GetAccountResult::FoundAccount(pk3, account3, true));
1572
1573 let pubkeys_str = vec![pk1.to_string(), pk2.to_string(), pk3.to_string()];
1576
1577 let response = setup
1578 .rpc
1579 .get_multiple_accounts(
1580 Some(setup.context),
1581 pubkeys_str,
1582 Some(RpcAccountInfoConfig::default()),
1583 )
1584 .await
1585 .unwrap();
1586
1587 assert_eq!(response.value.len(), 3);
1589
1590 println!("{:?}", response);
1591
1592 assert!(response.value[0].is_some());
1594 assert_eq!(
1595 response.value[0].as_ref().unwrap().lamports,
1596 1_000_000,
1597 "First element should be account1"
1598 );
1599
1600 assert!(
1602 response.value[1].is_none(),
1603 "Second element should be None for missing pk2"
1604 );
1605
1606 assert!(response.value[2].is_some());
1608 assert_eq!(
1609 response.value[2].as_ref().unwrap().lamports,
1610 3_000_000,
1611 "Third element should be account3"
1612 );
1613
1614 println!("✅ Account order preserved with remote: [1M lamports, None, 3M lamports]");
1615 }
1616}