From ee7f15eff1a033927d7bc851378b65c0baf687e0 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Tue, 12 May 2020 21:05:05 -0600 Subject: [PATCH] Rpc: optionally filter getLargestAccounts by circulating/nonCirculating (#10007) * Add circ/non-circ filter to getLargestAccounts * Plumb largest accounts into client and cli * Bump timeout toward CI flakiness * Update docs --- cli/src/cli.rs | 10 ++++ cli/src/cli_output.rs | 28 ++++++++- cli/src/cluster_query.rs | 52 +++++++++++++++++ client/src/lib.rs | 1 + client/src/rpc_client.rs | 8 +++ client/src/rpc_config.rs | 22 +++++++ client/src/rpc_request.rs | 3 + core/src/rpc.rs | 107 ++++++++++++++++++++++++++--------- core/tests/rpc.rs | 2 +- docs/src/apps/jsonrpc-api.md | 4 +- 10 files changed, 206 insertions(+), 31 deletions(-) create mode 100644 client/src/rpc_config.rs diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 6a7bd3f54..bb364759b 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -26,6 +26,7 @@ use solana_clap_utils::{ use solana_client::{ client_error::{ClientErrorKind, Result as ClientResult}, rpc_client::RpcClient, + rpc_config::RpcLargestAccountsFilter, rpc_response::{RpcAccount, RpcKeyedAccount}, }; #[cfg(not(test))] @@ -205,6 +206,10 @@ pub enum CliCommand { GetSlot { commitment_config: CommitmentConfig, }, + LargestAccounts { + commitment_config: CommitmentConfig, + filter: Option, + }, Supply { commitment_config: CommitmentConfig, print_accounts: bool, @@ -622,6 +627,7 @@ pub fn parse_command( }), ("epoch", Some(matches)) => parse_get_epoch(matches), ("slot", Some(matches)) => parse_get_slot(matches), + ("largest-accounts", Some(matches)) => parse_largest_accounts(matches), ("supply", Some(matches)) => parse_supply(matches), ("total-supply", Some(matches)) => parse_total_supply(matches), ("transaction-count", Some(matches)) => parse_get_transaction_count(matches), @@ -1707,6 +1713,10 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { CliCommand::GetSlot { commitment_config } => { process_get_slot(&rpc_client, *commitment_config) } + CliCommand::LargestAccounts { + commitment_config, + filter, + } => process_largest_accounts(&rpc_client, config, *commitment_config, filter.clone()), CliCommand::Supply { commitment_config, print_accounts, diff --git a/cli/src/cli_output.rs b/cli/src/cli_output.rs index 06f3d8418..685b278c0 100644 --- a/cli/src/cli_output.rs +++ b/cli/src/cli_output.rs @@ -4,7 +4,9 @@ use console::{style, Emoji}; use inflector::cases::titlecase::to_title_case; use serde::Serialize; use serde_json::{Map, Value}; -use solana_client::rpc_response::{RpcEpochInfo, RpcKeyedAccount, RpcSupply, RpcVoteAccountInfo}; +use solana_client::rpc_response::{ + RpcAccountBalance, RpcEpochInfo, RpcKeyedAccount, RpcSupply, RpcVoteAccountInfo, +}; use solana_sdk::{ clock::{self, Epoch, Slot, UnixTimestamp}, native_token::lamports_to_sol, @@ -909,6 +911,30 @@ impl fmt::Display for CliSignature { } } +#[derive(Serialize, Deserialize)] +pub struct CliAccountBalances { + pub accounts: Vec, +} + +impl fmt::Display for CliAccountBalances { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "{}", + style(format!("{:<44} {}", "Address", "Balance",)).bold() + )?; + for account in &self.accounts { + writeln!( + f, + "{:<44} {}", + account.address, + &format!("{} SOL", lamports_to_sol(account.lamports)) + )?; + } + Ok(()) + } +} + #[derive(Serialize, Deserialize)] pub struct CliSupply { pub total: u64, diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 5df4d1776..f649482e2 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -15,6 +15,7 @@ use solana_clap_utils::{ use solana_client::{ pubsub_client::{PubsubClient, SlotInfoMessage}, rpc_client::RpcClient, + rpc_config::{RpcLargestAccountsConfig, RpcLargestAccountsFilter}, rpc_request::MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS_SLOT_RANGE, }; use solana_remote_wallet::remote_wallet::RemoteWalletManager; @@ -119,6 +120,23 @@ impl ClusterQuerySubCommands for App<'_, '_> { SubCommand::with_name("epoch").about("Get current epoch") .arg(commitment_arg()), ) + .subcommand( + SubCommand::with_name("largest-accounts").about("Get addresses of largest cluster accounts") + .arg( + Arg::with_name("circulating") + .long("circulating") + .takes_value(false) + .help("Filter address list to only circulating accounts") + ) + .arg( + Arg::with_name("non_circulating") + .long("non-circulating") + .takes_value(false) + .conflicts_with("circulating") + .help("Filter address list to only non-circulating accounts") + ) + .arg(commitment_arg()), + ) .subcommand( SubCommand::with_name("supply").about("Get information about the cluster supply of SOL") .arg( @@ -352,6 +370,24 @@ pub fn parse_get_epoch(matches: &ArgMatches<'_>) -> Result) -> Result { + let commitment_config = commitment_of(matches, COMMITMENT_ARG.long).unwrap(); + let filter = if matches.is_present("circulating") { + Some(RpcLargestAccountsFilter::Circulating) + } else if matches.is_present("non_circulating") { + Some(RpcLargestAccountsFilter::NonCirculating) + } else { + None + }; + Ok(CliCommandInfo { + command: CliCommand::LargestAccounts { + commitment_config, + filter, + }, + signers: vec![], + }) +} + pub fn parse_supply(matches: &ArgMatches<'_>) -> Result { let commitment_config = commitment_of(matches, COMMITMENT_ARG.long).unwrap(); let print_accounts = matches.is_present("print_accounts"); @@ -815,6 +851,22 @@ pub fn process_show_block_production( Ok(config.output_format.formatted_string(&block_production)) } +pub fn process_largest_accounts( + rpc_client: &RpcClient, + config: &CliConfig, + commitment_config: CommitmentConfig, + filter: Option, +) -> ProcessResult { + let accounts = rpc_client + .get_largest_accounts_with_config(RpcLargestAccountsConfig { + commitment: Some(commitment_config), + filter, + })? + .value; + let largest_accounts = CliAccountBalances { accounts }; + Ok(config.output_format.formatted_string(&largest_accounts)) +} + pub fn process_supply( rpc_client: &RpcClient, config: &CliConfig, diff --git a/client/src/lib.rs b/client/src/lib.rs index dd4d3183b..90a4839c1 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -8,6 +8,7 @@ pub mod perf_utils; pub mod pubsub_client; pub mod rpc_client; pub mod rpc_client_request; +pub mod rpc_config; pub mod rpc_request; pub mod rpc_response; pub mod thin_client; diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 1868280bc..326359692 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -3,6 +3,7 @@ use crate::{ generic_rpc_client_request::GenericRpcClientRequest, mock_rpc_client_request::{MockRpcClientRequest, Mocks}, rpc_client_request::RpcClientRequest, + rpc_config::RpcLargestAccountsConfig, rpc_request::{RpcError, RpcRequest}, rpc_response::*, }; @@ -197,6 +198,13 @@ impl RpcClient { self.send(RpcRequest::GetTotalSupply, json!([commitment_config]), 0) } + pub fn get_largest_accounts_with_config( + &self, + config: RpcLargestAccountsConfig, + ) -> RpcResult> { + self.send(RpcRequest::GetLargestAccounts, json!([config]), 0) + } + pub fn get_vote_accounts(&self) -> ClientResult { self.get_vote_accounts_with_commitment(CommitmentConfig::default()) } diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs new file mode 100644 index 000000000..e43e3d3fd --- /dev/null +++ b/client/src/rpc_config.rs @@ -0,0 +1,22 @@ +use solana_sdk::commitment_config::CommitmentConfig; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcSignatureStatusConfig { + pub search_transaction_history: bool, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum RpcLargestAccountsFilter { + Circulating, + NonCirculating, +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcLargestAccountsConfig { + #[serde(flatten)] + pub commitment: Option, + pub filter: Option, +} diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index 7ce48e95a..ee1842f1e 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -19,6 +19,7 @@ pub enum RpcRequest { GetGenesisHash, GetIdentity, GetInflation, + GetLargestAccounts, GetLeaderSchedule, GetProgramAccounts, GetRecentBlockhash, @@ -62,6 +63,7 @@ impl fmt::Display for RpcRequest { RpcRequest::GetGenesisHash => "getGenesisHash", RpcRequest::GetIdentity => "getIdentity", RpcRequest::GetInflation => "getInflation", + RpcRequest::GetLargestAccounts => "getLargestAccounts", RpcRequest::GetLeaderSchedule => "getLeaderSchedule", RpcRequest::GetProgramAccounts => "getProgramAccounts", RpcRequest::GetRecentBlockhash => "getRecentBlockhash", @@ -91,6 +93,7 @@ impl fmt::Display for RpcRequest { } } +pub const NUM_LARGEST_ACCOUNTS: usize = 20; pub const MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS: usize = 256; pub const MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS_SLOT_RANGE: u64 = 10_000; diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 83e8fcfd8..75c794243 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -13,8 +13,10 @@ use bincode::serialize; use jsonrpc_core::{Error, Metadata, Result}; use jsonrpc_derive::rpc; use solana_client::{ + rpc_config::*, rpc_request::{ - MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS_SLOT_RANGE, MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, + MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS_SLOT_RANGE, + MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, NUM_LARGEST_ACCOUNTS, }, rpc_response::*, }; @@ -49,8 +51,6 @@ use std::{ time::{Duration, Instant}, }; -const NUM_LARGEST_ACCOUNTS: usize = 20; - type RpcResponse = Result>; fn new_response(bank: &Bank, value: T) -> RpcResponse { @@ -67,12 +67,6 @@ pub struct JsonRpcConfig { pub faucet_addr: Option, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RpcSignatureStatusConfig { - pub search_transaction_history: bool, -} - #[derive(Clone)] pub struct JsonRpcRequestProcessor { bank_forks: Arc>, @@ -281,22 +275,30 @@ impl JsonRpcRequestProcessor { fn get_largest_accounts( &self, - commitment: Option, + config: Option, ) -> RpcResponse> { - let bank = self.bank(commitment)?; + let config = config.unwrap_or_default(); + let bank = self.bank(config.commitment)?; + let (addresses, address_filter) = if let Some(filter) = config.filter { + let non_circulating_supply = calculate_non_circulating_supply(bank.clone()); + let addresses = non_circulating_supply.accounts.into_iter().collect(); + let address_filter = match filter { + RpcLargestAccountsFilter::Circulating => AccountAddressFilter::Exclude, + RpcLargestAccountsFilter::NonCirculating => AccountAddressFilter::Include, + }; + (addresses, address_filter) + } else { + (HashSet::new(), AccountAddressFilter::Exclude) + }; new_response( &bank, - bank.get_largest_accounts( - NUM_LARGEST_ACCOUNTS, - &HashSet::new(), - AccountAddressFilter::Exclude, - ) - .into_iter() - .map(|(address, lamports)| RpcAccountBalance { - address: address.to_string(), - lamports, - }) - .collect(), + bank.get_largest_accounts(NUM_LARGEST_ACCOUNTS, &addresses, address_filter) + .into_iter() + .map(|(address, lamports)| RpcAccountBalance { + address: address.to_string(), + lamports, + }) + .collect(), ) } @@ -838,7 +840,7 @@ pub trait RpcSol { fn get_largest_accounts( &self, meta: Self::Metadata, - commitment: Option, + config: Option, ) -> RpcResponse>; #[rpc(meta, name = "getSupply")] @@ -1255,13 +1257,13 @@ impl RpcSol for RpcSolImpl { fn get_largest_accounts( &self, meta: Self::Metadata, - commitment: Option, + config: Option, ) -> RpcResponse> { debug!("get_largest_accounts rpc request received"); meta.request_processor .read() .unwrap() - .get_largest_accounts(commitment) + .get_largest_accounts(config) } fn get_supply( @@ -1565,6 +1567,7 @@ pub mod tests { use super::*; use crate::{ commitment::BlockCommitment, contact_info::ContactInfo, + non_circulating_supply::non_circulating_accounts, replay_stage::tests::create_test_transactions_and_populate_blockstore, }; use bincode::deserialize; @@ -1717,6 +1720,9 @@ pub mod tests { let blockhash = bank.confirmed_last_blockhash().0; let tx = system_transaction::transfer(&alice, pubkey, 20, blockhash); bank.process_transaction(&tx).expect("process transaction"); + let tx = + system_transaction::transfer(&alice, &non_circulating_accounts()[0], 20, blockhash); + bank.process_transaction(&tx).expect("process transaction"); let tx = system_transaction::transfer(&alice, pubkey, std::u64::MAX, blockhash); let _ = bank.process_transaction(&tx); @@ -1868,7 +1874,7 @@ pub mod tests { let req = format!(r#"{{"jsonrpc":"2.0","id":1,"method":"getTransactionCount"}}"#); let res = io.handle_request_sync(&req, meta); - let expected = format!(r#"{{"jsonrpc":"2.0","result":3,"id":1}}"#); + let expected = format!(r#"{{"jsonrpc":"2.0","result":4,"id":1}}"#); let expected: Response = serde_json::from_str(&expected).expect("expected response deserialization"); let result: Response = serde_json::from_str(&res.expect("actual response")) @@ -1916,6 +1922,31 @@ pub mod tests { assert!(supply >= TEST_MINT_LAMPORTS); } + #[test] + fn test_get_supply() { + let bob_pubkey = Pubkey::new_rand(); + let RpcHandler { io, meta, .. } = start_rpc_handler_with_tx(&bob_pubkey); + let req = format!(r#"{{"jsonrpc":"2.0","id":1,"method":"getSupply"}}"#); + let res = io.handle_request_sync(&req, meta.clone()); + let json: Value = serde_json::from_str(&res.unwrap()).unwrap(); + let supply: RpcSupply = serde_json::from_value(json["result"]["value"].clone()) + .expect("actual response deserialization"); + assert_eq!(supply.non_circulating, 20); + assert!(supply.circulating >= TEST_MINT_LAMPORTS); + assert!(supply.total >= TEST_MINT_LAMPORTS + 20); + let expected_accounts: Vec = non_circulating_accounts() + .iter() + .map(|pubkey| pubkey.to_string()) + .collect(); + assert_eq!( + supply.non_circulating_accounts.len(), + expected_accounts.len() + ); + for address in supply.non_circulating_accounts { + assert!(expected_accounts.contains(&address)); + } + } + #[test] fn test_get_largest_accounts() { let bob_pubkey = Pubkey::new_rand(); @@ -1928,7 +1959,7 @@ pub mod tests { let largest_accounts: Vec = serde_json::from_value(json["result"]["value"].clone()) .expect("actual response deserialization"); - assert_eq!(largest_accounts.len(), 18); + assert_eq!(largest_accounts.len(), 19); // Get Alice balance let req = format!( @@ -1949,7 +1980,7 @@ pub mod tests { r#"{{"jsonrpc":"2.0","id":1,"method":"getBalance","params":["{}"]}}"#, bob_pubkey ); - let res = io.handle_request_sync(&req, meta); + let res = io.handle_request_sync(&req, meta.clone()); let json: Value = serde_json::from_str(&res.unwrap()).unwrap(); let bob_balance: u64 = serde_json::from_value(json["result"]["value"].clone()) .expect("actual response deserialization"); @@ -1957,6 +1988,26 @@ pub mod tests { address: bob_pubkey.to_string(), lamports: bob_balance, })); + + // Test Circulating/NonCirculating Filter + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getLargestAccounts","params":[{{"filter":"circulating"}}]}}"# + ); + let res = io.handle_request_sync(&req, meta.clone()); + let json: Value = serde_json::from_str(&res.unwrap()).unwrap(); + let largest_accounts: Vec = + serde_json::from_value(json["result"]["value"].clone()) + .expect("actual response deserialization"); + assert_eq!(largest_accounts.len(), 18); + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getLargestAccounts","params":[{{"filter":"nonCirculating"}}]}}"# + ); + let res = io.handle_request_sync(&req, meta.clone()); + let json: Value = serde_json::from_str(&res.unwrap()).unwrap(); + let largest_accounts: Vec = + serde_json::from_value(json["result"]["value"].clone()) + .expect("actual response deserialization"); + assert_eq!(largest_accounts.len(), 1); } #[test] diff --git a/core/tests/rpc.rs b/core/tests/rpc.rs index 8189cf28c..617dc0e18 100644 --- a/core/tests/rpc.rs +++ b/core/tests/rpc.rs @@ -265,7 +265,7 @@ fn test_rpc_subscriptions() { } // Wait for all signature subscriptions - let deadline = Instant::now() + Duration::from_secs(5); + let deadline = Instant::now() + Duration::from_secs(7); while !signature_set.is_empty() { let timeout = deadline.saturating_duration_since(Instant::now()); match status_receiver.recv_timeout(timeout) { diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index a35874c27..938ee88d7 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -642,7 +642,9 @@ Returns the 20 largest accounts, by lamport balance #### Parameters: -* `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) +* `` - (optional) Configuration object containing the following optional fields: + * (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + * (optional) `filter: ` - filter results by account type; currently supported: `circulating|nonCirculating` #### Results: