diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index 4317f73ab9..cf12d5c7ec 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -64,6 +64,20 @@ pub fn pubkey_of(matches: &ArgMatches<'_>, name: &str) -> Option { value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey())) } +pub fn pubkeys_of(matches: &ArgMatches<'_>, name: &str) -> Option> { + matches.values_of(name).map(|values| { + values + .map(|value| { + value.parse::().unwrap_or_else(|_| { + read_keypair_file(value) + .expect("read_keypair_file failed") + .pubkey() + }) + }) + .collect() + }) +} + // Return pubkey/signature pairs for a string of the form pubkey=signature pub fn pubkeys_sigs_of(matches: &ArgMatches<'_>, name: &str) -> Option> { matches.values_of(name).map(|values| { @@ -154,7 +168,7 @@ mod tests { #[test] fn test_keypair_of() { let keypair = Keypair::new(); - let outfile = tmp_file_path("test_gen_keypair_file.json", &keypair.pubkey()); + let outfile = tmp_file_path("test_keypair_of.json", &keypair.pubkey()); let _ = write_keypair_file(&keypair, &outfile).unwrap(); let matches = app() @@ -178,7 +192,7 @@ mod tests { #[test] fn test_pubkey_of() { let keypair = Keypair::new(); - let outfile = tmp_file_path("test_gen_keypair_file.json", &keypair.pubkey()); + let outfile = tmp_file_path("test_pubkey_of.json", &keypair.pubkey()); let _ = write_keypair_file(&keypair, &outfile).unwrap(); let matches = app() @@ -202,6 +216,26 @@ mod tests { fs::remove_file(&outfile).unwrap(); } + #[test] + fn test_pubkeys_of() { + let keypair = Keypair::new(); + let outfile = tmp_file_path("test_pubkeys_of.json", &keypair.pubkey()); + let _ = write_keypair_file(&keypair, &outfile).unwrap(); + + let matches = app().clone().get_matches_from(vec![ + "test", + "--multiple", + &keypair.pubkey().to_string(), + "--multiple", + &outfile, + ]); + assert_eq!( + pubkeys_of(&matches, "multiple"), + Some(vec![keypair.pubkey(), keypair.pubkey()]) + ); + fs::remove_file(&outfile).unwrap(); + } + #[test] fn test_pubkeys_sigs_of() { let key1 = Pubkey::new_rand(); diff --git a/cli/src/cli.rs b/cli/src/cli.rs index e67bf05645..fdc45a64df 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -126,6 +126,10 @@ pub enum CliCommand { slot_limit: Option, }, ShowGossip, + ShowStakes { + use_lamports_unit: bool, + vote_account_pubkeys: Option>, + }, ShowValidators { use_lamports_unit: bool, }, @@ -380,6 +384,7 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result parse_show_stakes(matches), ("show-validators", Some(matches)) => parse_show_validators(matches), // Nonce Commands ("authorize-nonce-account", Some(matches)) => parse_authorize_nonce_account(matches), @@ -1200,6 +1205,14 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { process_show_block_production(&rpc_client, config, *epoch, *slot_limit) } CliCommand::ShowGossip => process_show_gossip(&rpc_client), + CliCommand::ShowStakes { + use_lamports_unit, + vote_account_pubkeys, + } => process_show_stakes( + &rpc_client, + *use_lamports_unit, + vote_account_pubkeys.as_deref(), + ), CliCommand::ShowValidators { use_lamports_unit } => { process_show_validators(&rpc_client, *use_lamports_unit) } diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index ccc8eba3be..793d7c9d88 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -11,6 +11,7 @@ use indicatif::{ProgressBar, ProgressStyle}; use solana_clap_utils::{input_parsers::*, input_validators::*}; use solana_client::{rpc_client::RpcClient, rpc_response::RpcVoteAccountInfo}; use solana_sdk::{ + account_utils::State, clock::{self, Slot}, commitment_config::CommitmentConfig, epoch_schedule::{Epoch, EpochSchedule}, @@ -169,9 +170,28 @@ impl ClusterQuerySubCommands for App<'_, '_> { SubCommand::with_name("show-gossip") .about("Show the current gossip network nodes"), ) + .subcommand( + SubCommand::with_name("show-stakes") + .about("Show stake account information") + .arg( + Arg::with_name("vote_account_pubkeys") + .index(1) + .value_name("VOTE ACCOUNT PUBKEYS") + .takes_value(true) + .multiple(true) + .validator(is_pubkey_or_keypair) + .help("Only show stake accounts delegated to the provided vote accounts"), + ) + .arg( + Arg::with_name("lamports") + .long("lamports") + .takes_value(false) + .help("Display balance in lamports instead of SOL"), + ), + ) .subcommand( SubCommand::with_name("show-validators") - .about("Show information about the current validators") + .about("Show summary information about the current validators") .arg( Arg::with_name("lamports") .long("lamports") @@ -260,6 +280,19 @@ pub fn parse_get_transaction_count(matches: &ArgMatches<'_>) -> Result) -> Result { + let use_lamports_unit = matches.is_present("lamports"); + let vote_account_pubkeys = pubkeys_of(matches, "vote_account_pubkeys"); + + Ok(CliCommandInfo { + command: CliCommand::ShowStakes { + use_lamports_unit, + vote_account_pubkeys, + }, + require_keypair: false, + }) +} + pub fn parse_show_validators(matches: &ArgMatches<'_>) -> Result { let use_lamports_unit = matches.is_present("lamports"); @@ -754,6 +787,45 @@ pub fn process_show_gossip(rpc_client: &RpcClient) -> ProcessResult { )) } +pub fn process_show_stakes( + rpc_client: &RpcClient, + use_lamports_unit: bool, + vote_account_pubkeys: Option<&[Pubkey]>, +) -> ProcessResult { + use crate::stake::print_stake_state; + use solana_stake_program::stake_state::StakeState; + + let progress_bar = new_spinner_progress_bar(); + progress_bar.set_message("Fetching stake accounts..."); + let all_stake_accounts = rpc_client.get_program_accounts(&solana_stake_program::id())?; + progress_bar.finish_and_clear(); + + for (stake_pubkey, stake_account) in all_stake_accounts { + if let Ok(stake_state) = stake_account.state() { + match stake_state { + StakeState::Initialized(_) => { + if vote_account_pubkeys.is_none() { + println!("\nstake pubkey: {}", stake_pubkey); + print_stake_state(stake_account.lamports, &stake_state, use_lamports_unit); + } + } + StakeState::Stake(_, stake) => { + if vote_account_pubkeys.is_none() + || vote_account_pubkeys + .unwrap() + .contains(&stake.delegation.voter_pubkey) + { + println!("\nstake pubkey: {}", stake_pubkey); + print_stake_state(stake_account.lamports, &stake_state, use_lamports_unit); + } + } + _ => {} + } + } + } + Ok("".to_string()) +} + pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool) -> ProcessResult { let epoch_schedule = rpc_client.get_epoch_schedule()?; let vote_accounts = rpc_client.get_vote_accounts()?; diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 7ead7ccdc7..2fdac9f2a1 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -24,10 +24,9 @@ use solana_sdk::{ }, transaction::Transaction, }; -use solana_stake_program::stake_state::Meta; use solana_stake_program::{ stake_instruction::{self, StakeError}, - stake_state::{Authorized, Lockup, StakeAuthorize, StakeState}, + stake_state::{Authorized, Lockup, Meta, StakeAuthorize, StakeState}, }; use solana_vote_program::vote_state::VoteState; use std::ops::Deref; @@ -969,20 +968,7 @@ pub fn process_redeem_vote_credits( log_instruction_custom_error::(result) } -pub fn process_show_stake_account( - rpc_client: &RpcClient, - _config: &CliConfig, - stake_account_pubkey: &Pubkey, - use_lamports_unit: bool, -) -> ProcessResult { - let stake_account = rpc_client.get_account(stake_account_pubkey)?; - if stake_account.owner != solana_stake_program::id() { - return Err(CliError::RpcRequestError(format!( - "{:?} is not a stake account", - stake_account_pubkey - )) - .into()); - } +pub fn print_stake_state(stake_lamports: u64, stake_state: &StakeState, use_lamports_unit: bool) { fn show_authorized(authorized: &Authorized) { println!("authorized staker: {}", authorized.staker); println!("authorized withdrawer: {}", authorized.staker); @@ -991,16 +977,16 @@ pub fn process_show_stake_account( println!("lockup epoch: {}", lockup.epoch); println!("lockup custodian: {}", lockup.custodian); } - match stake_account.state() { - Ok(StakeState::Stake( + match stake_state { + StakeState::Stake( Meta { authorized, lockup, .. }, stake, - )) => { + ) => { println!( "total stake: {}", - build_balance_message(stake_account.lamports, use_lamports_unit, true) + build_balance_message(stake_lamports, use_lamports_unit, true) ); println!("credits observed: {}", stake.credits_observed); println!( @@ -1026,16 +1012,40 @@ pub fn process_show_stake_account( } show_authorized(&authorized); show_lockup(&lockup); - Ok("".to_string()) } - Ok(StakeState::RewardsPool) => Ok("Stake account is a rewards pool".to_string()), - Ok(StakeState::Uninitialized) => Ok("Stake account is uninitialized".to_string()), - Ok(StakeState::Initialized(Meta { + StakeState::RewardsPool => println!("stake account is a rewards pool"), + StakeState::Uninitialized => println!("stake account is uninitialized"), + StakeState::Initialized(Meta { authorized, lockup, .. - })) => { - println!("Stake account is undelegated"); + }) => { + println!( + "total stake: {}", + build_balance_message(stake_lamports, use_lamports_unit, true) + ); + println!("stake account is undelegated"); show_authorized(&authorized); show_lockup(&lockup); + } + } +} + +pub fn process_show_stake_account( + rpc_client: &RpcClient, + _config: &CliConfig, + stake_account_pubkey: &Pubkey, + use_lamports_unit: bool, +) -> ProcessResult { + let stake_account = rpc_client.get_account(stake_account_pubkey)?; + if stake_account.owner != solana_stake_program::id() { + return Err(CliError::RpcRequestError(format!( + "{:?} is not a stake account", + stake_account_pubkey + )) + .into()); + } + match stake_account.state() { + Ok(stake_state) => { + print_stake_state(stake_account.lamports, &stake_state, use_lamports_unit); Ok("".to_string()) } Err(err) => Err(CliError::RpcRequestError(format!(