1#![allow(clippy::unit_cmp)]
2
3use jsonrpc_core::{BoxFuture, Error as JsonRpcCoreError, ErrorCode, Result};
4use jsonrpc_derive::rpc;
5use solana_client::{
6 rpc_config::{
7 RpcAccountInfoConfig, RpcLargestAccountsConfig, RpcProgramAccountsConfig, RpcSupplyConfig,
8 RpcTokenAccountsFilter,
9 },
10 rpc_request::TokenAccountsFilter,
11 rpc_response::{
12 OptionalContext, RpcAccountBalance, RpcKeyedAccount, RpcResponseContext, RpcSupply,
13 RpcTokenAccountBalance,
14 },
15};
16use solana_commitment_config::CommitmentConfig;
17use solana_rpc_client_api::response::Response as RpcResponse;
18
19use super::{RunloopContext, State, SurfnetRpcContext, utils::verify_pubkey};
20use crate::surfnet::locker::SvmAccessContext;
21
22#[rpc]
23pub trait AccountsScan {
24 type Metadata;
25
26 #[rpc(meta, name = "getProgramAccounts")]
107 fn get_program_accounts(
108 &self,
109 meta: Self::Metadata,
110 program_id_str: String,
111 config: Option<RpcProgramAccountsConfig>,
112 ) -> BoxFuture<Result<OptionalContext<Vec<RpcKeyedAccount>>>>;
113
114 #[rpc(meta, name = "getLargestAccounts")]
169 fn get_largest_accounts(
170 &self,
171 meta: Self::Metadata,
172 config: Option<RpcLargestAccountsConfig>,
173 ) -> BoxFuture<Result<RpcResponse<Vec<RpcAccountBalance>>>>;
174
175 #[rpc(meta, name = "getSupply")]
231 fn get_supply(
232 &self,
233 meta: Self::Metadata,
234 config: Option<RpcSupplyConfig>,
235 ) -> BoxFuture<Result<RpcResponse<RpcSupply>>>;
236
237 #[rpc(meta, name = "getTokenLargestAccounts")]
300 fn get_token_largest_accounts(
301 &self,
302 meta: Self::Metadata,
303 mint_str: String,
304 commitment: Option<CommitmentConfig>,
305 ) -> BoxFuture<Result<RpcResponse<Vec<RpcTokenAccountBalance>>>>;
306
307 #[rpc(meta, name = "getTokenAccountsByOwner")]
378 fn get_token_accounts_by_owner(
379 &self,
380 meta: Self::Metadata,
381 owner_str: String,
382 token_account_filter: RpcTokenAccountsFilter,
383 config: Option<RpcAccountInfoConfig>,
384 ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>>;
385
386 #[rpc(meta, name = "getTokenAccountsByDelegate")]
449 fn get_token_accounts_by_delegate(
450 &self,
451 meta: Self::Metadata,
452 delegate_str: String,
453 token_account_filter: RpcTokenAccountsFilter,
454 config: Option<RpcAccountInfoConfig>,
455 ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>>;
456}
457
458#[derive(Clone)]
459pub struct SurfpoolAccountsScanRpc;
460impl AccountsScan for SurfpoolAccountsScanRpc {
461 type Metadata = Option<RunloopContext>;
462
463 fn get_program_accounts(
464 &self,
465 meta: Self::Metadata,
466 program_id_str: String,
467 config: Option<RpcProgramAccountsConfig>,
468 ) -> BoxFuture<Result<OptionalContext<Vec<RpcKeyedAccount>>>> {
469 let config = config.unwrap_or_default();
470 let program_id = match verify_pubkey(&program_id_str) {
471 Ok(res) => res,
472 Err(e) => return e.into(),
473 };
474
475 let SurfnetRpcContext {
476 svm_locker,
477 remote_ctx,
478 } = match meta.get_rpc_context(()) {
479 Ok(res) => res,
480 Err(e) => return e.into(),
481 };
482
483 Box::pin(async move {
484 let current_slot = svm_locker.get_latest_absolute_slot();
485
486 let account_config = config.account_config;
487
488 if let Some(min_context_slot_val) = account_config.min_context_slot.as_ref() {
489 if current_slot < *min_context_slot_val {
490 return Err(JsonRpcCoreError {
491 code: ErrorCode::InternalError,
492 message: format!(
493 "Node's current slot {} is less than requested minContextSlot {}",
494 current_slot, min_context_slot_val
495 ),
496 data: None,
497 });
498 }
499 }
500
501 let program_accounts = svm_locker
503 .get_program_accounts(
504 &remote_ctx.map(|(client, _)| client),
505 &program_id,
506 account_config,
507 config.filters,
508 )
509 .await?
510 .inner;
511
512 if config.with_context.unwrap_or(false) {
513 Ok(OptionalContext::Context(RpcResponse {
514 context: RpcResponseContext::new(current_slot),
515 value: program_accounts,
516 }))
517 } else {
518 Ok(OptionalContext::NoContext(program_accounts))
519 }
520 })
521 }
522
523 fn get_largest_accounts(
524 &self,
525 meta: Self::Metadata,
526 config: Option<RpcLargestAccountsConfig>,
527 ) -> BoxFuture<Result<RpcResponse<Vec<RpcAccountBalance>>>> {
528 let config = config.unwrap_or_default();
529 let SurfnetRpcContext {
530 svm_locker,
531 remote_ctx,
532 } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
533 Ok(res) => res,
534 Err(e) => return e.into(),
535 };
536
537 Box::pin(async move {
538 let SvmAccessContext {
539 slot,
540 inner: largest_accounts,
541 ..
542 } = svm_locker.get_largest_accounts(&remote_ctx, config).await?;
543
544 Ok(RpcResponse {
545 context: RpcResponseContext::new(slot),
546 value: largest_accounts,
547 })
548 })
549 }
550
551 fn get_supply(
552 &self,
553 meta: Self::Metadata,
554 config: Option<RpcSupplyConfig>,
555 ) -> BoxFuture<Result<RpcResponse<RpcSupply>>> {
556 let svm_locker = match meta.get_svm_locker() {
557 Ok(locker) => locker,
558 Err(e) => return e.into(),
559 };
560
561 Box::pin(async move {
562 svm_locker.with_svm_reader(|svm_reader| {
563 let slot = svm_reader.get_latest_absolute_slot();
564
565 let exclude_accounts = config
567 .as_ref()
568 .map(|c| c.exclude_non_circulating_accounts_list)
569 .unwrap_or(false);
570
571 Ok(RpcResponse {
572 context: RpcResponseContext::new(slot),
573 value: RpcSupply {
574 total: svm_reader.total_supply,
575 circulating: svm_reader.circulating_supply,
576 non_circulating: svm_reader.non_circulating_supply,
577 non_circulating_accounts: if exclude_accounts {
578 vec![]
579 } else {
580 svm_reader.non_circulating_accounts.clone()
581 },
582 },
583 })
584 })
585 })
586 }
587
588 fn get_token_largest_accounts(
589 &self,
590 meta: Self::Metadata,
591 mint_str: String,
592 commitment: Option<CommitmentConfig>,
593 ) -> BoxFuture<Result<RpcResponse<Vec<RpcTokenAccountBalance>>>> {
594 let mint = match verify_pubkey(&mint_str) {
595 Ok(res) => res,
596 Err(e) => return e.into(),
597 };
598
599 let SurfnetRpcContext {
600 svm_locker,
601 remote_ctx,
602 } = match meta.get_rpc_context(commitment.unwrap_or_default()) {
603 Ok(res) => res,
604 Err(e) => return e.into(),
605 };
606
607 Box::pin(async move {
608 let SvmAccessContext {
609 slot,
610 inner: largest_accounts,
611 ..
612 } = svm_locker
613 .get_token_largest_accounts(&remote_ctx, &mint)
614 .await?;
615
616 Ok(RpcResponse {
617 context: RpcResponseContext::new(slot),
618 value: largest_accounts,
619 })
620 })
621 }
622
623 fn get_token_accounts_by_owner(
624 &self,
625 meta: Self::Metadata,
626 owner_str: String,
627 token_account_filter: RpcTokenAccountsFilter,
628 config: Option<RpcAccountInfoConfig>,
629 ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>> {
630 let config = config.unwrap_or_default();
631 let owner = match verify_pubkey(&owner_str) {
632 Ok(res) => res,
633 Err(e) => return e.into(),
634 };
635
636 let filter = match token_account_filter {
637 RpcTokenAccountsFilter::Mint(mint_str) => {
638 let mint = match verify_pubkey(&mint_str) {
639 Ok(res) => res,
640 Err(e) => return e.into(),
641 };
642 TokenAccountsFilter::Mint(mint)
643 }
644 RpcTokenAccountsFilter::ProgramId(program_id_str) => {
645 let program_id = match verify_pubkey(&program_id_str) {
646 Ok(res) => res,
647 Err(e) => return e.into(),
648 };
649 TokenAccountsFilter::ProgramId(program_id)
650 }
651 };
652
653 let SurfnetRpcContext {
654 svm_locker,
655 remote_ctx,
656 } = match meta.get_rpc_context(()) {
657 Ok(res) => res,
658 Err(e) => return e.into(),
659 };
660
661 Box::pin(async move {
662 let SvmAccessContext {
663 slot,
664 inner: token_accounts,
665 ..
666 } = svm_locker
667 .get_token_accounts_by_owner(
668 &remote_ctx.map(|(client, _)| client),
669 owner,
670 &filter,
671 &config,
672 )
673 .await?;
674
675 Ok(RpcResponse {
676 context: RpcResponseContext::new(slot),
677 value: token_accounts,
678 })
679 })
680 }
681
682 fn get_token_accounts_by_delegate(
683 &self,
684 meta: Self::Metadata,
685 delegate_str: String,
686 token_account_filter: RpcTokenAccountsFilter,
687 config: Option<RpcAccountInfoConfig>,
688 ) -> BoxFuture<Result<RpcResponse<Vec<RpcKeyedAccount>>>> {
689 let config = config.unwrap_or_default();
690 let delegate = match verify_pubkey(&delegate_str) {
691 Ok(res) => res,
692 Err(e) => return e.into(),
693 };
694
695 let SurfnetRpcContext {
696 svm_locker,
697 remote_ctx,
698 } = match meta.get_rpc_context(config.commitment.unwrap_or_default()) {
699 Ok(res) => res,
700 Err(e) => return e.into(),
701 };
702
703 Box::pin(async move {
704 let filter = match token_account_filter {
705 RpcTokenAccountsFilter::Mint(mint_str) => {
706 TokenAccountsFilter::Mint(verify_pubkey(&mint_str)?)
707 }
708 RpcTokenAccountsFilter::ProgramId(program_id_str) => {
709 TokenAccountsFilter::ProgramId(verify_pubkey(&program_id_str)?)
710 }
711 };
712
713 let remote_ctx = remote_ctx.map(|(r, _)| r);
714 let SvmAccessContext {
715 slot,
716 inner: keyed_accounts,
717 ..
718 } = svm_locker
719 .get_token_accounts_by_delegate(&remote_ctx, delegate, &filter, &config)
720 .await?;
721
722 Ok(RpcResponse {
723 context: RpcResponseContext::new(slot),
724 value: keyed_accounts,
725 })
726 })
727 }
728}
729
730#[cfg(test)]
731mod tests {
732
733 use core::panic;
734 use std::str::FromStr;
735
736 use itertools::Itertools;
737 use solana_account::Account;
738 use solana_client::{
739 rpc_config::{
740 RpcLargestAccountsConfig, RpcLargestAccountsFilter, RpcProgramAccountsConfig,
741 RpcSupplyConfig, RpcTokenAccountsFilter,
742 },
743 rpc_filter::{Memcmp, RpcFilterType},
744 rpc_response::OptionalContext,
745 };
746 use solana_program_pack::Pack;
747 use solana_pubkey::Pubkey;
748 use spl_token_interface::state::Account as TokenAccount;
749 use surfpool_types::SupplyUpdate;
750
751 use super::{AccountsScan, SurfpoolAccountsScanRpc};
752 use crate::{
753 rpc::surfnet_cheatcodes::{SurfnetCheatcodes, SurfnetCheatcodesRpc},
754 tests::helpers::TestSetup,
755 };
756
757 const VALID_PUBKEY_1: &str = "11111111111111111111111111111112";
758
759 #[tokio::test(flavor = "multi_thread")]
760 async fn test_get_program_accounts() {
761 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
762
763 let owner_pubkey = Pubkey::new_unique();
765 let owned_pubkey_short_data = Pubkey::new_unique();
767 let owner_pubkey_long_data = Pubkey::new_unique();
768 let other_pubkey = Pubkey::new_unique();
770
771 setup.context.svm_locker.with_svm_writer(|svm_writer| {
772 svm_writer
773 .set_account(
774 &owned_pubkey_short_data,
775 Account {
776 lamports: 1000,
777 data: vec![4, 5, 6],
778 owner: owner_pubkey,
779 executable: false,
780 rent_epoch: 0,
781 },
782 )
783 .unwrap();
784
785 svm_writer
786 .set_account(
787 &owner_pubkey_long_data,
788 Account {
789 lamports: 2000,
790 data: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
791 owner: owner_pubkey,
792 executable: false,
793 rent_epoch: 0,
794 },
795 )
796 .unwrap();
797
798 svm_writer
799 .set_account(
800 &other_pubkey,
801 Account {
802 lamports: 500,
803 data: vec![4, 5, 6],
804 owner: Pubkey::new_unique(),
805 executable: false,
806 rent_epoch: 0,
807 },
808 )
809 .unwrap();
810 });
811
812 {
814 let res = setup
815 .rpc
816 .get_program_accounts(Some(setup.context.clone()), owner_pubkey.to_string(), None)
817 .await
818 .expect("Failed to get program accounts");
819 match res {
820 OptionalContext::Context(_) => {
821 panic!("Expected no context");
822 }
823 OptionalContext::NoContext(value) => {
824 assert_eq!(value.len(), 2);
825
826 let short_data_account = value
827 .iter()
828 .find(|acc| acc.pubkey == owned_pubkey_short_data.to_string())
829 .expect("Short data account not found");
830 assert_eq!(short_data_account.account.lamports, 1000);
831
832 let long_data_account = value
833 .iter()
834 .find(|acc| acc.pubkey == owner_pubkey_long_data.to_string())
835 .expect("Long data account not found");
836 assert_eq!(long_data_account.account.lamports, 2000);
837 }
838 }
839 }
840
841 {
843 let res = setup
844 .rpc
845 .get_program_accounts(
846 Some(setup.context.clone()),
847 owner_pubkey.to_string(),
848 Some(RpcProgramAccountsConfig {
849 filters: Some(vec![RpcFilterType::DataSize(3)]),
850 with_context: Some(true),
851 ..Default::default()
852 }),
853 )
854 .await
855 .expect("Failed to get program accounts with data size filter");
856
857 match res {
858 OptionalContext::Context(response) => {
859 assert_eq!(response.value.len(), 1);
860
861 let short_data_account = response
862 .value
863 .iter()
864 .find(|acc| acc.pubkey == owned_pubkey_short_data.to_string())
865 .expect("Short data account not found");
866 assert_eq!(short_data_account.account.lamports, 1000);
867 }
868 OptionalContext::NoContext(_) => {
869 panic!("Expected context");
870 }
871 }
872 }
873
874 {
876 let res = setup
877 .rpc
878 .get_program_accounts(
879 Some(setup.context.clone()),
880 owner_pubkey.to_string(),
881 Some(RpcProgramAccountsConfig {
882 filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
883 1,
884 vec![5, 6],
885 ))]),
886 with_context: Some(false),
887 ..Default::default()
888 }),
889 )
890 .await
891 .expect("Failed to get program accounts with memcmp filter");
892
893 match res {
894 OptionalContext::Context(_) => {
895 panic!("Expected no context");
896 }
897 OptionalContext::NoContext(value) => {
898 assert_eq!(value.len(), 1);
899
900 let short_data_account = value
901 .iter()
902 .find(|acc| acc.pubkey == owned_pubkey_short_data.to_string())
903 .expect("Short data account not found");
904 assert_eq!(short_data_account.account.lamports, 1000);
905 }
906 }
907 }
908 }
909
910 #[tokio::test(flavor = "multi_thread")]
911 async fn test_set_and_get_supply() {
912 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
913 let cheatcodes_rpc = SurfnetCheatcodesRpc::empty();
914
915 let initial_supply = setup
917 .rpc
918 .get_supply(Some(setup.context.clone()), None)
919 .await
920 .unwrap();
921
922 assert_eq!(initial_supply.value.total, 0);
923 assert_eq!(initial_supply.value.circulating, 0);
924 assert_eq!(initial_supply.value.non_circulating, 0);
925 assert_eq!(initial_supply.value.non_circulating_accounts.len(), 0);
926
927 let supply_update = SupplyUpdate {
929 total: Some(1_000_000_000_000_000),
930 circulating: Some(800_000_000_000_000),
931 non_circulating: Some(200_000_000_000_000),
932 non_circulating_accounts: Some(vec![
933 VALID_PUBKEY_1.to_string(),
934 VALID_PUBKEY_1.to_string(),
935 ]),
936 };
937
938 let set_result = cheatcodes_rpc
939 .set_supply(Some(setup.context.clone()), supply_update)
940 .await
941 .unwrap();
942
943 assert_eq!(set_result.value, ());
944
945 let supply = setup
947 .rpc
948 .get_supply(Some(setup.context.clone()), None)
949 .await
950 .unwrap();
951
952 assert_eq!(supply.value.total, 1_000_000_000_000_000);
953 assert_eq!(supply.value.circulating, 800_000_000_000_000);
954 assert_eq!(supply.value.non_circulating, 200_000_000_000_000);
955 assert_eq!(supply.value.non_circulating_accounts.len(), 2);
956 assert_eq!(supply.value.non_circulating_accounts[0], VALID_PUBKEY_1);
957 }
958
959 #[tokio::test(flavor = "multi_thread")]
960 async fn test_get_supply_exclude_accounts() {
961 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
962 let cheatcodes_rpc = SurfnetCheatcodesRpc::empty();
963
964 let supply_update = SupplyUpdate {
966 total: Some(1_000_000_000_000_000),
967 circulating: Some(800_000_000_000_000),
968 non_circulating: Some(200_000_000_000_000),
969 non_circulating_accounts: Some(vec![
970 VALID_PUBKEY_1.to_string(),
971 VALID_PUBKEY_1.to_string(),
972 ]),
973 };
974
975 cheatcodes_rpc
976 .set_supply(Some(setup.context.clone()), supply_update)
977 .await
978 .unwrap();
979
980 let config_exclude = RpcSupplyConfig {
982 commitment: None,
983 exclude_non_circulating_accounts_list: true,
984 };
985
986 let supply_excluded = setup
987 .rpc
988 .get_supply(Some(setup.context.clone()), Some(config_exclude))
989 .await
990 .unwrap();
991
992 assert_eq!(supply_excluded.value.total, 1_000_000_000_000_000);
993 assert_eq!(supply_excluded.value.circulating, 800_000_000_000_000);
994 assert_eq!(supply_excluded.value.non_circulating, 200_000_000_000_000);
995 assert_eq!(supply_excluded.value.non_circulating_accounts.len(), 0); let supply_included = setup
999 .rpc
1000 .get_supply(Some(setup.context.clone()), None)
1001 .await
1002 .unwrap();
1003
1004 assert_eq!(supply_included.value.non_circulating_accounts.len(), 2);
1005 }
1006
1007 #[tokio::test(flavor = "multi_thread")]
1008 async fn test_partial_supply_update() {
1009 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1010 let cheatcodes_rpc = SurfnetCheatcodesRpc::empty();
1011
1012 let initial_update = SupplyUpdate {
1014 total: Some(1_000_000_000_000_000),
1015 circulating: Some(800_000_000_000_000),
1016 non_circulating: Some(200_000_000_000_000),
1017 non_circulating_accounts: Some(vec![VALID_PUBKEY_1.to_string()]),
1018 };
1019
1020 cheatcodes_rpc
1021 .set_supply(Some(setup.context.clone()), initial_update)
1022 .await
1023 .unwrap();
1024
1025 let partial_update = SupplyUpdate {
1027 total: Some(2_000_000_000_000_000),
1028 circulating: None,
1029 non_circulating: None,
1030 non_circulating_accounts: None,
1031 };
1032
1033 cheatcodes_rpc
1034 .set_supply(Some(setup.context.clone()), partial_update)
1035 .await
1036 .unwrap();
1037
1038 let supply = setup
1040 .rpc
1041 .get_supply(Some(setup.context), None)
1042 .await
1043 .unwrap();
1044
1045 assert_eq!(supply.value.total, 2_000_000_000_000_000); assert_eq!(supply.value.circulating, 800_000_000_000_000);
1047 assert_eq!(supply.value.non_circulating, 200_000_000_000_000);
1048 assert_eq!(supply.value.non_circulating_accounts.len(), 1);
1049 }
1050
1051 #[tokio::test(flavor = "multi_thread")]
1052 async fn test_set_supply_with_multiple_invalid_pubkeys() {
1053 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1054 let cheatcodes_rpc = SurfnetCheatcodesRpc::empty();
1055 let invalid_pubkey = "invalid_pubkey";
1056
1057 let supply_update = SupplyUpdate {
1059 total: Some(1_000_000_000_000_000),
1060 circulating: Some(800_000_000_000_000),
1061 non_circulating: Some(200_000_000_000_000),
1062 non_circulating_accounts: Some(vec![
1063 VALID_PUBKEY_1.to_string(), invalid_pubkey.to_string(), "also_invalid".to_string(), ]),
1067 };
1068
1069 let result = cheatcodes_rpc
1070 .set_supply(Some(setup.context), supply_update)
1071 .await;
1072
1073 assert!(result.is_err());
1074 let error = result.unwrap_err();
1075 assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
1076 assert_eq!(
1077 error.message,
1078 format!("Invalid pubkey '{}' at index 1", invalid_pubkey)
1079 );
1080 }
1081
1082 #[tokio::test(flavor = "multi_thread")]
1083 async fn test_set_supply_with_max_values() {
1084 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1085 let cheatcodes_rpc = SurfnetCheatcodesRpc::empty();
1086
1087 let supply_update = SupplyUpdate {
1088 total: Some(u64::MAX),
1089 circulating: Some(u64::MAX - 1),
1090 non_circulating: Some(1),
1091 non_circulating_accounts: Some(vec![VALID_PUBKEY_1.to_string()]),
1092 };
1093
1094 let result = cheatcodes_rpc
1095 .set_supply(Some(setup.context.clone()), supply_update)
1096 .await;
1097
1098 assert!(result.is_ok());
1099
1100 let supply = setup
1101 .rpc
1102 .get_supply(Some(setup.context), None)
1103 .await
1104 .unwrap();
1105
1106 assert_eq!(supply.value.total, u64::MAX);
1107 assert_eq!(supply.value.circulating, u64::MAX - 1);
1108 assert_eq!(supply.value.non_circulating, 1);
1109 assert_eq!(supply.value.non_circulating_accounts[0], VALID_PUBKEY_1);
1110 }
1111
1112 #[tokio::test(flavor = "multi_thread")]
1113 async fn test_set_supply_large_valid_account_list() {
1114 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1115 let cheatcodes_rpc = SurfnetCheatcodesRpc::empty();
1116
1117 let large_account_list: Vec<String> = (0..100)
1118 .map(|i| match i % 10 {
1119 0 => "3rSZJHysEk2ueFVovRLtZ8LGnQBMZGg96H2Q4jErspAF".to_string(),
1120 1 => "11111111111111111111111111111111".to_string(),
1121 2 => "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA".to_string(),
1122 3 => "So11111111111111111111111111111111111111112".to_string(),
1123 4 => "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1124 5 => "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB".to_string(),
1125 6 => "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R".to_string(),
1126 7 => "9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E".to_string(),
1127 8 => "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk".to_string(),
1128 _ => "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263".to_string(),
1129 })
1130 .collect();
1131
1132 let supply_update = SupplyUpdate {
1133 total: Some(1_000_000_000_000_000),
1134 circulating: Some(800_000_000_000_000),
1135 non_circulating: Some(200_000_000_000_000),
1136 non_circulating_accounts: Some(large_account_list.clone()),
1137 };
1138
1139 let result = cheatcodes_rpc
1140 .set_supply(Some(setup.context.clone()), supply_update)
1141 .await;
1142
1143 assert!(result.is_ok());
1144
1145 let supply = setup
1146 .rpc
1147 .get_supply(Some(setup.context), None)
1148 .await
1149 .unwrap();
1150
1151 assert_eq!(supply.value.non_circulating_accounts.len(), 100);
1152 assert_eq!(
1153 supply.value.non_circulating_accounts[0],
1154 "3rSZJHysEk2ueFVovRLtZ8LGnQBMZGg96H2Q4jErspAF"
1155 );
1156 assert_eq!(
1157 supply.value.non_circulating_accounts[1],
1158 "11111111111111111111111111111111"
1159 );
1160 }
1161
1162 #[tokio::test(flavor = "multi_thread")]
1163 async fn test_get_largest_accounts() {
1164 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1165
1166 let large_circulating_pubkey = Pubkey::new_unique();
1167 let large_circulating_amount = 1_000_000_000_000_000u64;
1168
1169 setup
1170 .context
1171 .svm_locker
1172 .with_svm_writer(|svm_writer| {
1173 svm_writer.set_account(
1174 &large_circulating_pubkey,
1175 Account {
1176 lamports: large_circulating_amount,
1177 ..Default::default()
1178 },
1179 )
1180 })
1181 .unwrap();
1182
1183 let large_non_circulating_pubkey = Pubkey::new_unique();
1184 let large_non_circulating_amount = 2_000_000_000_000_000u64;
1185
1186 setup
1187 .context
1188 .svm_locker
1189 .with_svm_writer(|svm_writer| {
1190 svm_writer
1191 .non_circulating_accounts
1192 .push(large_non_circulating_pubkey.to_string());
1193 svm_writer.set_account(
1194 &large_non_circulating_pubkey,
1195 Account {
1196 lamports: large_non_circulating_amount,
1197 ..Default::default()
1198 },
1199 )
1200 })
1201 .unwrap();
1202
1203 {
1205 let result = setup
1206 .rpc
1207 .get_largest_accounts(
1208 Some(setup.context.clone()),
1209 Some(RpcLargestAccountsConfig {
1210 filter: Some(RpcLargestAccountsFilter::Circulating),
1211 ..Default::default()
1212 }),
1213 )
1214 .await
1215 .unwrap();
1216
1217 assert_eq!(result.value.len(), 20);
1218 assert!(
1219 result
1220 .value
1221 .iter()
1222 .any(|bal| bal.address == large_circulating_pubkey.to_string()
1223 && bal.lamports == large_circulating_amount),
1224 "Circulating account should be in circulating accounts list"
1225 );
1226 assert_eq!(large_circulating_amount, result.value[0].lamports);
1227 assert!(
1228 !result
1229 .value
1230 .iter()
1231 .any(|bal| bal.address == large_non_circulating_pubkey.to_string()),
1232 "Non-circulating account should not be in circulating accounts list"
1233 );
1234 }
1235
1236 {
1238 let result = setup
1239 .rpc
1240 .get_largest_accounts(
1241 Some(setup.context.clone()),
1242 Some(RpcLargestAccountsConfig {
1243 filter: Some(RpcLargestAccountsFilter::NonCirculating),
1244 ..Default::default()
1245 }),
1246 )
1247 .await
1248 .unwrap();
1249
1250 assert_eq!(result.value.len(), 1);
1251 assert!(
1252 result.value.iter().any(|bal| bal.address
1253 == large_non_circulating_pubkey.to_string()
1254 && bal.lamports == large_non_circulating_amount),
1255 "Non-circulating account should be in non-circulating accounts list: {:?}",
1256 result.value
1257 );
1258 }
1259
1260 {
1262 let result = setup
1263 .rpc
1264 .get_largest_accounts(Some(setup.context), None)
1265 .await
1266 .unwrap();
1267
1268 assert_eq!(result.value.len(), 20);
1269 let circulating_idx = result
1270 .value
1271 .iter()
1272 .find_position(|bal| {
1273 bal.address == large_circulating_pubkey.to_string()
1274 && bal.lamports == large_circulating_amount
1275 })
1276 .expect("Circulating account should be in list")
1277 .0;
1278 let non_circulating_idx = result
1279 .value
1280 .iter()
1281 .find_position(|bal| {
1282 bal.address == large_non_circulating_pubkey.to_string()
1283 && bal.lamports == large_non_circulating_amount
1284 })
1285 .expect("Non-circulating account should be in list")
1286 .0;
1287
1288 assert!(
1289 non_circulating_idx < circulating_idx,
1290 "Circulating account should appear before non-circulating account in combined list"
1291 );
1292 }
1293 }
1294
1295 #[tokio::test(flavor = "multi_thread")]
1296 async fn test_get_supply_with_invalid_config() {
1297 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1298
1299 let config = RpcSupplyConfig {
1300 commitment: Some(solana_commitment_config::CommitmentConfig {
1301 commitment: solana_commitment_config::CommitmentLevel::Processed,
1302 }),
1303 exclude_non_circulating_accounts_list: false,
1304 };
1305
1306 let result = setup
1307 .rpc
1308 .get_supply(Some(setup.context), Some(config))
1309 .await;
1310
1311 assert!(result.is_ok());
1312 let supply = result.unwrap();
1313 assert_eq!(supply.value.total, 0);
1314 assert_eq!(supply.value.circulating, 0);
1315 assert_eq!(supply.value.non_circulating, 0);
1316 }
1317
1318 #[tokio::test(flavor = "multi_thread")]
1319 async fn test_get_token_largest_accounts_local_svm() {
1320 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1321
1322 let mint_pk = Pubkey::new_unique();
1324 let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
1325 svm_reader
1326 .inner
1327 .minimum_balance_for_rent_exemption(spl_token_interface::state::Mint::LEN)
1328 });
1329
1330 let mut mint_data = [0; spl_token_interface::state::Mint::LEN];
1332 let mint = spl_token_interface::state::Mint {
1333 decimals: 9,
1334 supply: 1000000000000000,
1335 is_initialized: true,
1336 ..Default::default()
1337 };
1338 mint.pack_into_slice(&mut mint_data);
1339
1340 let mint_account = Account {
1341 lamports: minimum_rent,
1342 owner: spl_token_interface::ID,
1343 executable: false,
1344 rent_epoch: 0,
1345 data: mint_data.to_vec(),
1346 };
1347
1348 let token_accounts = vec![
1350 (Pubkey::new_unique(), 1000000000), (Pubkey::new_unique(), 5000000000), (Pubkey::new_unique(), 500000000), (Pubkey::new_unique(), 2000000000), (Pubkey::new_unique(), 100000000), ];
1356
1357 setup.context.svm_locker.with_svm_writer(|svm_writer| {
1358 svm_writer
1360 .set_account(&mint_pk, mint_account.clone())
1361 .unwrap();
1362
1363 for (token_account_pk, amount) in &token_accounts {
1365 let mut token_account_data = [0; TokenAccount::LEN];
1366 let token_account = TokenAccount {
1367 mint: mint_pk,
1368 owner: Pubkey::new_unique(),
1369 amount: *amount,
1370 delegate: solana_program_option::COption::None,
1371 state: spl_token_interface::state::AccountState::Initialized,
1372 is_native: solana_program_option::COption::None,
1373 delegated_amount: 0,
1374 close_authority: solana_program_option::COption::None,
1375 };
1376 token_account.pack_into_slice(&mut token_account_data);
1377
1378 let account = Account {
1379 lamports: minimum_rent,
1380 owner: spl_token_interface::ID,
1381 executable: false,
1382 rent_epoch: 0,
1383 data: token_account_data.to_vec(),
1384 };
1385
1386 svm_writer.set_account(token_account_pk, account).unwrap();
1387 }
1388 });
1389
1390 let result = setup
1391 .rpc
1392 .get_token_largest_accounts(Some(setup.context), mint_pk.to_string(), None)
1393 .await
1394 .unwrap();
1395
1396 assert_eq!(result.value.len(), 5);
1397
1398 assert_eq!(result.value[0].amount.amount, "5000000000"); assert_eq!(result.value[1].amount.amount, "2000000000"); assert_eq!(result.value[2].amount.amount, "1000000000"); assert_eq!(result.value[3].amount.amount, "500000000"); assert_eq!(result.value[4].amount.amount, "100000000"); for balance in &result.value {
1407 assert_eq!(balance.amount.decimals, 9);
1408 assert!(balance.amount.ui_amount.is_some());
1409 assert!(!balance.amount.ui_amount_string.is_empty());
1410 }
1411
1412 for balance in &result.value {
1414 assert!(Pubkey::from_str(&balance.address).is_ok());
1415 }
1416 }
1417
1418 #[tokio::test(flavor = "multi_thread")]
1419 async fn test_get_token_largest_accounts_limit_to_20() {
1420 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1421
1422 let mint_pk = Pubkey::new_unique();
1424 let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
1425 svm_reader
1426 .inner
1427 .minimum_balance_for_rent_exemption(spl_token_interface::state::Mint::LEN)
1428 });
1429
1430 let mut mint_data = [0; spl_token_interface::state::Mint::LEN];
1432 let mint = spl_token_interface::state::Mint {
1433 decimals: 6,
1434 supply: 1000000000000000,
1435 is_initialized: true,
1436 ..Default::default()
1437 };
1438 mint.pack_into_slice(&mut mint_data);
1439
1440 let mint_account = Account {
1441 lamports: minimum_rent,
1442 owner: spl_token_interface::ID,
1443 executable: false,
1444 rent_epoch: 0,
1445 data: mint_data.to_vec(),
1446 };
1447
1448 let mut token_accounts = Vec::new();
1450 for i in 0..25 {
1451 token_accounts.push((Pubkey::new_unique(), (i + 1) * 1000000)); }
1453
1454 setup.context.svm_locker.with_svm_writer(|svm_writer| {
1455 svm_writer
1457 .set_account(&mint_pk, mint_account.clone())
1458 .unwrap();
1459
1460 for (token_account_pk, amount) in &token_accounts {
1462 let mut token_account_data = [0; TokenAccount::LEN];
1463 let token_account = TokenAccount {
1464 mint: mint_pk,
1465 owner: Pubkey::new_unique(),
1466 amount: *amount,
1467 delegate: solana_program_option::COption::None,
1468 state: spl_token_interface::state::AccountState::Initialized,
1469 is_native: solana_program_option::COption::None,
1470 delegated_amount: 0,
1471 close_authority: solana_program_option::COption::None,
1472 };
1473 token_account.pack_into_slice(&mut token_account_data);
1474
1475 let account = Account {
1476 lamports: minimum_rent,
1477 owner: spl_token_interface::ID,
1478 executable: false,
1479 rent_epoch: 0,
1480 data: token_account_data.to_vec(),
1481 };
1482
1483 svm_writer.set_account(token_account_pk, account).unwrap();
1484 }
1485 });
1486
1487 let result = setup
1489 .rpc
1490 .get_token_largest_accounts(Some(setup.context), mint_pk.to_string(), None)
1491 .await
1492 .unwrap();
1493
1494 assert_eq!(result.value.len(), 20);
1496
1497 assert_eq!(result.value[0].amount.amount, "25000000"); assert_eq!(result.value[1].amount.amount, "24000000"); assert_eq!(result.value[19].amount.amount, "6000000"); for balance in &result.value {
1504 assert_eq!(balance.amount.decimals, 6);
1505 assert!(balance.amount.ui_amount.is_some());
1506 assert!(!balance.amount.ui_amount_string.is_empty());
1507 assert!(Pubkey::from_str(&balance.address).is_ok());
1508 }
1509 }
1510
1511 #[tokio::test(flavor = "multi_thread")]
1512 async fn test_get_token_largest_accounts_edge_cases() {
1513 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1514
1515 let invalid_result = setup
1517 .rpc
1518 .get_token_largest_accounts(
1519 Some(setup.context.clone()),
1520 "invalid_pubkey".to_string(),
1521 None,
1522 )
1523 .await;
1524 assert!(invalid_result.is_err());
1525 let error = invalid_result.unwrap_err();
1526 assert_eq!(error.code, jsonrpc_core::ErrorCode::InvalidParams);
1527
1528 let empty_mint_pk = Pubkey::new_unique();
1530 let minimum_rent = setup.context.svm_locker.with_svm_reader(|svm_reader| {
1531 svm_reader
1532 .inner
1533 .minimum_balance_for_rent_exemption(spl_token_interface::state::Mint::LEN)
1534 });
1535
1536 let mut mint_data = [0; spl_token_interface::state::Mint::LEN];
1538 let mint = spl_token_interface::state::Mint {
1539 decimals: 9,
1540 supply: 0,
1541 is_initialized: true,
1542 ..Default::default()
1543 };
1544 mint.pack_into_slice(&mut mint_data);
1545
1546 let mint_account = Account {
1547 lamports: minimum_rent,
1548 owner: spl_token_interface::ID,
1549 executable: false,
1550 rent_epoch: 0,
1551 data: mint_data.to_vec(),
1552 };
1553
1554 setup.context.svm_locker.with_svm_writer(|svm_writer| {
1555 svm_writer
1556 .set_account(&empty_mint_pk, mint_account.clone())
1557 .unwrap();
1558 });
1559
1560 let empty_result = setup
1561 .rpc
1562 .get_token_largest_accounts(
1563 Some(setup.context.clone()),
1564 empty_mint_pk.to_string(),
1565 None,
1566 )
1567 .await
1568 .unwrap();
1569
1570 assert_eq!(empty_result.value.len(), 0);
1572
1573 let nonexistent_mint_pk = Pubkey::new_unique();
1575 let nonexistent_result = setup
1576 .rpc
1577 .get_token_largest_accounts(Some(setup.context), nonexistent_mint_pk.to_string(), None)
1578 .await
1579 .unwrap();
1580
1581 assert_eq!(nonexistent_result.value.len(), 0);
1583 }
1584
1585 #[tokio::test(flavor = "multi_thread")]
1586 async fn test_get_token_accounts_by_delegate() {
1587 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1588
1589 let delegate = Pubkey::new_unique();
1590 let owner = Pubkey::new_unique();
1591 let mint = Pubkey::new_unique();
1592 let token_account_pubkey = Pubkey::new_unique();
1593 let token_program = spl_token_interface::id();
1594
1595 let mut token_account_data = [0u8; spl_token_interface::state::Account::LEN];
1597 let token_account = spl_token_interface::state::Account {
1598 mint,
1599 owner,
1600 amount: 1000,
1601 delegate: solana_program_option::COption::Some(delegate),
1602 state: spl_token_interface::state::AccountState::Initialized,
1603 is_native: solana_program_option::COption::None,
1604 delegated_amount: 500,
1605 close_authority: solana_program_option::COption::None,
1606 };
1607 solana_program_pack::Pack::pack_into_slice(&token_account, &mut token_account_data);
1608
1609 let account = Account {
1610 lamports: 1000000,
1611 data: token_account_data.to_vec(),
1612 owner: token_program,
1613 executable: false,
1614 rent_epoch: 0,
1615 };
1616
1617 setup.context.svm_locker.with_svm_writer(|svm_writer| {
1618 svm_writer
1619 .set_account(&token_account_pubkey, account.clone())
1620 .unwrap();
1621 });
1622
1623 let result = setup
1625 .rpc
1626 .get_token_accounts_by_delegate(
1627 Some(setup.context.clone()),
1628 delegate.to_string(),
1629 RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1630 None,
1631 )
1632 .await;
1633
1634 assert!(result.is_ok(), "ProgramId filter should succeed");
1635 let response = result.unwrap();
1636 assert_eq!(response.value.len(), 1, "Should find 1 token account");
1637 assert_eq!(response.value[0].pubkey, token_account_pubkey.to_string());
1638
1639 let result = setup
1641 .rpc
1642 .get_token_accounts_by_delegate(
1643 Some(setup.context.clone()),
1644 delegate.to_string(),
1645 RpcTokenAccountsFilter::Mint(mint.to_string()),
1646 None,
1647 )
1648 .await;
1649
1650 assert!(result.is_ok(), "Mint filter should succeed");
1651 let response = result.unwrap();
1652 assert_eq!(response.value.len(), 1, "Should find 1 token account");
1653 assert_eq!(response.value[0].pubkey, token_account_pubkey.to_string());
1654
1655 let non_existent_delegate = Pubkey::new_unique();
1657 let result = setup
1658 .rpc
1659 .get_token_accounts_by_delegate(
1660 Some(setup.context.clone()),
1661 non_existent_delegate.to_string(),
1662 RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1663 None,
1664 )
1665 .await;
1666
1667 assert!(result.is_ok(), "Non-existent delegate should succeed");
1668 let response = result.unwrap();
1669 assert_eq!(response.value.len(), 0, "Should find 0 token accounts");
1670
1671 let wrong_mint = Pubkey::new_unique();
1673 let result = setup
1674 .rpc
1675 .get_token_accounts_by_delegate(
1676 Some(setup.context.clone()),
1677 delegate.to_string(),
1678 RpcTokenAccountsFilter::Mint(wrong_mint.to_string()),
1679 None,
1680 )
1681 .await;
1682
1683 assert!(result.is_ok(), "Wrong mint should succeed");
1684 let response = result.unwrap();
1685 assert_eq!(response.value.len(), 0, "Should find 0 token accounts");
1686
1687 let result = setup
1689 .rpc
1690 .get_token_accounts_by_delegate(
1691 Some(setup.context.clone()),
1692 "invalid_pubkey".to_string(),
1693 RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1694 None,
1695 )
1696 .await;
1697
1698 assert!(result.is_err(), "Invalid pubkey should fail");
1699 }
1700
1701 #[tokio::test(flavor = "multi_thread")]
1702 async fn test_get_token_accounts_by_delegate_multiple_accounts() {
1703 let setup = TestSetup::new(SurfpoolAccountsScanRpc);
1704
1705 let delegate = Pubkey::new_unique();
1706 let owner1 = Pubkey::new_unique();
1707 let owner2 = Pubkey::new_unique();
1708 let mint1 = Pubkey::new_unique();
1709 let mint2 = Pubkey::new_unique();
1710 let token_account1 = Pubkey::new_unique();
1711 let token_account2 = Pubkey::new_unique();
1712 let token_program = spl_token_interface::id();
1713
1714 let mut token_account_data1 = [0u8; spl_token_interface::state::Account::LEN];
1716 let token_account_struct1 = spl_token_interface::state::Account {
1717 mint: mint1,
1718 owner: owner1,
1719 amount: 1000,
1720 delegate: solana_program_option::COption::Some(delegate),
1721 state: spl_token_interface::state::AccountState::Initialized,
1722 is_native: solana_program_option::COption::None,
1723 delegated_amount: 500,
1724 close_authority: solana_program_option::COption::None,
1725 };
1726 solana_program_pack::Pack::pack_into_slice(
1727 &token_account_struct1,
1728 &mut token_account_data1,
1729 );
1730
1731 let mut token_account_data2 = [0u8; spl_token_interface::state::Account::LEN];
1733 let token_account_struct2 = spl_token_interface::state::Account {
1734 mint: mint2,
1735 owner: owner2,
1736 amount: 2000,
1737 delegate: solana_program_option::COption::Some(delegate),
1738 state: spl_token_interface::state::AccountState::Initialized,
1739 is_native: solana_program_option::COption::None,
1740 delegated_amount: 1000,
1741 close_authority: solana_program_option::COption::None,
1742 };
1743 solana_program_pack::Pack::pack_into_slice(
1744 &token_account_struct2,
1745 &mut token_account_data2,
1746 );
1747
1748 setup.context.svm_locker.with_svm_writer(|svm_writer| {
1749 svm_writer
1750 .set_account(
1751 &token_account1,
1752 Account {
1753 lamports: 1000000,
1754 data: token_account_data1.to_vec(),
1755 owner: token_program,
1756 executable: false,
1757 rent_epoch: 0,
1758 },
1759 )
1760 .unwrap();
1761
1762 svm_writer
1763 .set_account(
1764 &token_account2,
1765 Account {
1766 lamports: 1000000,
1767 data: token_account_data2.to_vec(),
1768 owner: token_program,
1769 executable: false,
1770 rent_epoch: 0,
1771 },
1772 )
1773 .unwrap();
1774 });
1775
1776 let result = setup
1777 .rpc
1778 .get_token_accounts_by_delegate(
1779 Some(setup.context.clone()),
1780 delegate.to_string(),
1781 RpcTokenAccountsFilter::ProgramId(token_program.to_string()),
1782 None,
1783 )
1784 .await;
1785
1786 assert!(result.is_ok(), "ProgramId filter should succeed");
1787 let response = result.unwrap();
1788 assert_eq!(response.value.len(), 2, "Should find 2 token accounts");
1789
1790 let returned_pubkeys: std::collections::HashSet<String> = response
1791 .value
1792 .iter()
1793 .map(|acc| acc.pubkey.clone())
1794 .collect();
1795 assert!(returned_pubkeys.contains(&token_account1.to_string()));
1796 assert!(returned_pubkeys.contains(&token_account2.to_string()));
1797
1798 let result = setup
1800 .rpc
1801 .get_token_accounts_by_delegate(
1802 Some(setup.context.clone()),
1803 delegate.to_string(),
1804 RpcTokenAccountsFilter::Mint(mint1.to_string()),
1805 None,
1806 )
1807 .await;
1808
1809 assert!(result.is_ok(), "Mint filter should succeed");
1810 let response = result.unwrap();
1811 assert_eq!(
1812 response.value.len(),
1813 1,
1814 "Should find 1 token account for mint1"
1815 );
1816 assert_eq!(response.value[0].pubkey, token_account1.to_string());
1817 }
1818}