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