From 5298e3872cb16c5cd53b1cf776f900ab082dcfeb Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Tue, 14 Apr 2020 13:10:25 -0600 Subject: [PATCH] Cli: enable json output (#9478) automerge --- Cargo.lock | 22 +- cli/Cargo.toml | 2 +- cli/src/cli.rs | 60 +-- cli/src/cli_output.rs | 841 ++++++++++++++++++++++++++++++++++++++ cli/src/cluster_query.rs | 306 ++++---------- cli/src/display.rs | 10 + cli/src/lib.rs | 4 + cli/src/main.rs | 19 + cli/src/nonce.rs | 51 +-- cli/src/stake.rs | 142 +++---- cli/src/validator_info.rs | 28 +- cli/src/vote.rs | 68 ++- 12 files changed, 1136 insertions(+), 417 deletions(-) create mode 100644 cli/src/cli_output.rs diff --git a/Cargo.lock b/Cargo.lock index 1f1cdb4fae..e0c83f8251 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,14 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "adler32" version = "1.0.4" @@ -3827,6 +3836,7 @@ dependencies = [ name = "solana-cli" version = "1.2.0" dependencies = [ + "Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)", "bincode 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "bs58 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3862,7 +3872,6 @@ dependencies = [ "solana-vote-signer 1.2.0", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)", - "titlecase 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -5486,15 +5495,6 @@ dependencies = [ "crunchy 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "titlecase" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "tokio" version = "0.1.22" @@ -6280,6 +6280,7 @@ dependencies = [ ] [metadata] +"checksum Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)" = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" "checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" "checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" "checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" @@ -6748,7 +6749,6 @@ dependencies = [ "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" "checksum tiny-bip39 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1cd1fb03fe8e07d17cd851a624a9fff74642a997b67fbd1ccd77533241640d92" "checksum tiny-keccak 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d8a021c69bb74a44ccedb824a046447e2c84a01df9e5c20779750acb38e11b2" -"checksum titlecase 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f565e410cfc24c2f2a89960b023ca192689d7f77d3f8d4f4af50c2d8affe1117" "checksum tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" "checksum tokio 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0e1bef565a52394086ecac0a6fa3b8ace4cb3a138ee1d96bd2b93283b56824e3" "checksum tokio-buf 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8fb220f46c53859a4b7ec083e41dec9778ff0b1851c0942b211edb89e0ccdc46" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a9609ac43b..5318ca6f39 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -18,6 +18,7 @@ ctrlc = { version = "3.1.4", features = ["termination"] } console = "0.10.0" dirs = "2.0.2" log = "0.4.8" +Inflector = "0.11.4" indicatif = "0.14.0" humantime = "2.0.0" num-traits = "0.2" @@ -41,7 +42,6 @@ solana-stake-program = { path = "../programs/stake", version = "1.2.0" } solana-storage-program = { path = "../programs/storage", version = "1.2.0" } solana-vote-program = { path = "../programs/vote", version = "1.2.0" } solana-vote-signer = { path = "../vote-signer", version = "1.2.0" } -titlecase = "1.1.0" thiserror = "1.0.15" url = "2.1.1" diff --git a/cli/src/cli.rs b/cli/src/cli.rs index a0e5c9e067..e4a5fdfc05 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1,4 +1,5 @@ use crate::{ + cli_output::{CliAccount, OutputFormat}, cluster_query::*, display::{println_name_value, println_signers}, nonce::{self, *}, @@ -21,6 +22,7 @@ use solana_clap_utils::{ use solana_client::{ client_error::{ClientErrorKind, Result as ClientResult}, rpc_client::RpcClient, + rpc_response::{RpcAccount, RpcKeyedAccount}, }; #[cfg(not(test))] use solana_faucet::faucet::request_airdrop_transaction; @@ -472,6 +474,7 @@ pub struct CliConfig<'a> { pub keypair_path: String, pub rpc_client: Option, pub verbose: bool, + pub output_format: OutputFormat, } impl CliConfig<'_> { @@ -563,6 +566,7 @@ impl Default for CliConfig<'_> { keypair_path: Self::default_keypair_path(), rpc_client: None, verbose: false, + output_format: OutputFormat::Display, } } } @@ -1179,31 +1183,33 @@ fn process_confirm(rpc_client: &RpcClient, signature: &Signature) -> ProcessResu fn process_show_account( rpc_client: &RpcClient, - _config: &CliConfig, + config: &CliConfig, account_pubkey: &Pubkey, output_file: &Option, use_lamports_unit: bool, ) -> ProcessResult { let account = rpc_client.get_account(account_pubkey)?; + let data = account.data.clone(); + let cli_account = CliAccount { + keyed_account: RpcKeyedAccount { + pubkey: account_pubkey.to_string(), + account: RpcAccount::encode(account), + }, + use_lamports_unit, + }; - println!(); - println_name_value("Public Key:", &account_pubkey.to_string()); - println_name_value( - "Balance:", - &build_balance_message(account.lamports, use_lamports_unit, true), - ); - println_name_value("Owner:", &account.owner.to_string()); - println_name_value("Executable:", &account.executable.to_string()); - println_name_value("Rent Epoch:", &account.rent_epoch.to_string()); + config.output_format.formatted_print(&cli_account); - if let Some(output_file) = output_file { - let mut f = File::create(output_file)?; - f.write_all(&account.data)?; - println!(); - println!("Wrote account data to {}", output_file); - } else if !account.data.is_empty() { - use pretty_hex::*; - println!("{:?}", account.data.hex_dump()); + if config.output_format == OutputFormat::Display { + if let Some(output_file) = output_file { + let mut f = File::create(output_file)?; + f.write_all(&data)?; + println!(); + println!("Wrote account data to {}", output_file); + } else if !data.is_empty() { + use pretty_hex::*; + println!("{:?}", data.hex_dump()); + } } Ok("".to_string()) @@ -1577,7 +1583,7 @@ fn process_witness( } pub fn process_command(config: &CliConfig) -> ProcessResult { - if config.verbose { + if config.verbose && config.output_format == OutputFormat::Display { println_name_value("RPC URL:", &config.json_rpc_url); println_name_value("Default Signer Path:", &config.keypair_path); if config.keypair_path.starts_with("usb://") { @@ -1622,7 +1628,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { CliCommand::GetBlockTime { slot } => process_get_block_time(&rpc_client, *slot), CliCommand::GetGenesisHash => process_get_genesis_hash(&rpc_client), CliCommand::GetEpochInfo { commitment_config } => { - process_get_epoch_info(&rpc_client, *commitment_config) + process_get_epoch_info(&rpc_client, config, *commitment_config) } CliCommand::GetEpoch { commitment_config } => { process_get_epoch(&rpc_client, *commitment_config) @@ -1662,13 +1668,14 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { vote_account_pubkeys, } => process_show_stakes( &rpc_client, + config, *use_lamports_unit, vote_account_pubkeys.as_deref(), ), CliCommand::ShowValidators { use_lamports_unit, commitment_config, - } => process_show_validators(&rpc_client, *use_lamports_unit, *commitment_config), + } => process_show_validators(&rpc_client, config, *use_lamports_unit, *commitment_config), // Nonce Commands @@ -1711,7 +1718,12 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { CliCommand::ShowNonceAccount { nonce_account_pubkey, use_lamports_unit, - } => process_show_nonce_account(&rpc_client, &nonce_account_pubkey, *use_lamports_unit), + } => process_show_nonce_account( + &rpc_client, + config, + &nonce_account_pubkey, + *use_lamports_unit, + ), // Withdraw lamports from a nonce account CliCommand::WithdrawFromNonceAccount { nonce_account, @@ -1940,7 +1952,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { // Return all or single validator info CliCommand::GetValidatorInfo(info_pubkey) => { - process_get_validator_info(&rpc_client, *info_pubkey) + process_get_validator_info(&rpc_client, config, *info_pubkey) } // Publish validator info CliCommand::SetValidatorInfo { @@ -2532,7 +2544,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' ) .arg( Arg::with_name("output_file") - .long("output") + .long("output-file") .short("o") .value_name("FILEPATH") .takes_value(true) diff --git a/cli/src/cli_output.rs b/cli/src/cli_output.rs new file mode 100644 index 0000000000..c691f37f0e --- /dev/null +++ b/cli/src/cli_output.rs @@ -0,0 +1,841 @@ +use crate::{cli::build_balance_message, display::writeln_name_value}; +use chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc}; +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, RpcVoteAccountInfo}; +use solana_sdk::{ + clock::{self, Epoch, Slot, UnixTimestamp}, + stake_history::StakeHistoryEntry, +}; +use solana_stake_program::stake_state::{Authorized, Lockup}; +use solana_vote_program::{ + authorized_voters::AuthorizedVoters, + vote_state::{BlockTimestamp, Lockout}, +}; +use std::{collections::BTreeMap, fmt, time::Duration}; + +static WARNING: Emoji = Emoji("⚠️", "!"); + +#[derive(PartialEq)] +pub enum OutputFormat { + Display, + Json, + JsonCompact, +} + +impl OutputFormat { + pub fn formatted_print(&self, item: &T) + where + T: Serialize + fmt::Display, + { + match self { + OutputFormat::Display => { + println!("{}", item); + } + OutputFormat::Json => { + println!("{}", serde_json::to_string_pretty(item).unwrap()); + } + OutputFormat::JsonCompact => { + println!("{}", serde_json::to_value(item).unwrap()); + } + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct CliAccount { + #[serde(flatten)] + pub keyed_account: RpcKeyedAccount, + #[serde(skip_serializing)] + pub use_lamports_unit: bool, +} + +impl fmt::Display for CliAccount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f)?; + writeln_name_value(f, "Public Key:", &self.keyed_account.pubkey)?; + writeln_name_value( + f, + "Balance:", + &build_balance_message( + self.keyed_account.account.lamports, + self.use_lamports_unit, + true, + ), + )?; + writeln_name_value(f, "Owner:", &self.keyed_account.account.owner)?; + writeln_name_value( + f, + "Executable:", + &self.keyed_account.account.executable.to_string(), + )?; + writeln_name_value( + f, + "Rent Epoch:", + &self.keyed_account.account.rent_epoch.to_string(), + )?; + Ok(()) + } +} + +#[derive(Default, Serialize, Deserialize)] +pub struct CliBlockProduction { + pub epoch: Epoch, + pub start_slot: Slot, + pub end_slot: Slot, + pub total_slots: usize, + pub total_blocks_produced: usize, + pub total_slots_skipped: usize, + pub leaders: Vec, + pub individual_slot_status: Vec, + #[serde(skip_serializing)] + pub verbose: bool, +} + +impl fmt::Display for CliBlockProduction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f)?; + writeln!( + f, + "{}", + style(format!( + " {:<44} {:>15} {:>15} {:>15} {:>23}", + "Identity Pubkey", + "Leader Slots", + "Blocks Produced", + "Skipped Slots", + "Skipped Slot Percentage", + )) + .bold() + )?; + for leader in &self.leaders { + writeln!( + f, + " {:<44} {:>15} {:>15} {:>15} {:>22.2}%", + leader.identity_pubkey, + leader.leader_slots, + leader.blocks_produced, + leader.skipped_slots, + leader.skipped_slots as f64 / leader.leader_slots as f64 * 100. + )?; + } + writeln!(f)?; + writeln!( + f, + " {:<44} {:>15} {:>15} {:>15} {:>22.2}%", + format!("Epoch {} total:", self.epoch), + self.total_slots, + self.total_blocks_produced, + self.total_slots_skipped, + self.total_slots_skipped as f64 / self.total_slots as f64 * 100. + )?; + writeln!( + f, + " (using data from {} slots: {} to {})", + self.total_slots, self.start_slot, self.end_slot + )?; + if self.verbose { + writeln!(f)?; + writeln!(f)?; + writeln!( + f, + "{}", + style(format!(" {:<15} {:<44}", "Slot", "Identity Pubkey")).bold(), + )?; + for status in &self.individual_slot_status { + if status.skipped { + writeln!( + f, + "{}", + style(format!( + " {:<15} {:<44} SKIPPED", + status.slot, status.leader + )) + .red() + )?; + } else { + writeln!( + f, + "{}", + style(format!(" {:<15} {:<44}", status.slot, status.leader)) + )?; + } + } + } + Ok(()) + } +} + +#[derive(Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliBlockProductionEntry { + pub identity_pubkey: String, + pub leader_slots: u64, + pub blocks_produced: u64, + pub skipped_slots: u64, +} + +#[derive(Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliSlotStatus { + pub slot: Slot, + pub leader: String, + pub skipped: bool, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliEpochInfo { + #[serde(flatten)] + pub epoch_info: RpcEpochInfo, +} + +impl From for CliEpochInfo { + fn from(epoch_info: RpcEpochInfo) -> Self { + Self { epoch_info } + } +} + +impl fmt::Display for CliEpochInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f)?; + writeln_name_value(f, "Slot:", &self.epoch_info.absolute_slot.to_string())?; + writeln_name_value(f, "Epoch:", &self.epoch_info.epoch.to_string())?; + let start_slot = self.epoch_info.absolute_slot - self.epoch_info.slot_index; + let end_slot = start_slot + self.epoch_info.slots_in_epoch; + writeln_name_value( + f, + "Epoch Slot Range:", + &format!("[{}..{})", start_slot, end_slot), + )?; + writeln_name_value( + f, + "Epoch Completed Percent:", + &format!( + "{:>3.3}%", + self.epoch_info.slot_index as f64 / self.epoch_info.slots_in_epoch as f64 * 100_f64 + ), + )?; + let remaining_slots_in_epoch = self.epoch_info.slots_in_epoch - self.epoch_info.slot_index; + writeln_name_value( + f, + "Epoch Completed Slots:", + &format!( + "{}/{} ({} remaining)", + self.epoch_info.slot_index, + self.epoch_info.slots_in_epoch, + remaining_slots_in_epoch + ), + )?; + writeln_name_value( + f, + "Epoch Completed Time:", + &format!( + "{}/{} ({} remaining)", + slot_to_human_time(self.epoch_info.slot_index), + slot_to_human_time(self.epoch_info.slots_in_epoch), + slot_to_human_time(remaining_slots_in_epoch) + ), + ) + } +} + +fn slot_to_human_time(slot: Slot) -> String { + humantime::format_duration(Duration::from_secs( + slot * clock::DEFAULT_TICKS_PER_SLOT / clock::DEFAULT_TICKS_PER_SECOND, + )) + .to_string() +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliValidators { + pub total_active_stake: u64, + pub total_current_stake: u64, + pub total_deliquent_stake: u64, + pub current_validators: Vec, + pub delinquent_validators: Vec, + #[serde(skip_serializing)] + pub use_lamports_unit: bool, +} + +impl fmt::Display for CliValidators { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn write_vote_account( + f: &mut fmt::Formatter, + validator: &CliValidator, + total_active_stake: u64, + use_lamports_unit: bool, + delinquent: bool, + ) -> fmt::Result { + fn non_zero_or_dash(v: u64) -> String { + if v == 0 { + "-".into() + } else { + format!("{}", v) + } + } + + writeln!( + f, + "{} {:<44} {:<44} {:>9}% {:>8} {:>10} {:>7} {}", + if delinquent { + WARNING.to_string() + } else { + " ".to_string() + }, + validator.identity_pubkey, + validator.vote_account_pubkey, + validator.commission, + non_zero_or_dash(validator.last_vote), + non_zero_or_dash(validator.root_slot), + validator.credits, + if validator.activated_stake > 0 { + format!( + "{} ({:.2}%)", + build_balance_message(validator.activated_stake, use_lamports_unit, true), + 100. * validator.activated_stake as f64 / total_active_stake as f64 + ) + } else { + "-".into() + }, + ) + } + writeln_name_value( + f, + "Active Stake:", + &build_balance_message(self.total_active_stake, self.use_lamports_unit, true), + )?; + if self.total_deliquent_stake > 0 { + writeln_name_value( + f, + "Current Stake:", + &format!( + "{} ({:0.2}%)", + &build_balance_message(self.total_current_stake, self.use_lamports_unit, true), + 100. * self.total_current_stake as f64 / self.total_active_stake as f64 + ), + )?; + writeln_name_value( + f, + "Delinquent Stake:", + &format!( + "{} ({:0.2}%)", + &build_balance_message( + self.total_deliquent_stake, + self.use_lamports_unit, + true + ), + 100. * self.total_deliquent_stake as f64 / self.total_active_stake as f64 + ), + )?; + } + writeln!(f)?; + writeln!( + f, + "{}", + style(format!( + " {:<44} {:<44} {} {} {} {:>7} {}", + "Identity Pubkey", + "Vote Account Pubkey", + "Commission", + "Last Vote", + "Root Block", + "Credits", + "Active Stake", + )) + .bold() + )?; + for validator in &self.current_validators { + write_vote_account( + f, + validator, + self.total_active_stake, + self.use_lamports_unit, + false, + )?; + } + for validator in &self.delinquent_validators { + write_vote_account( + f, + validator, + self.total_active_stake, + self.use_lamports_unit, + true, + )?; + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliValidator { + pub identity_pubkey: String, + pub vote_account_pubkey: String, + pub commission: u8, + pub last_vote: u64, + pub root_slot: u64, + pub credits: u64, + pub activated_stake: u64, +} + +impl CliValidator { + pub fn new(vote_account: &RpcVoteAccountInfo, current_epoch: Epoch) -> Self { + Self { + identity_pubkey: vote_account.node_pubkey.to_string(), + vote_account_pubkey: vote_account.vote_pubkey.to_string(), + commission: vote_account.commission, + last_vote: vote_account.last_vote, + root_slot: vote_account.root_slot, + credits: vote_account + .epoch_credits + .iter() + .find_map(|(epoch, credits, _)| { + if *epoch == current_epoch { + Some(*credits) + } else { + None + } + }) + .unwrap_or(0), + activated_stake: vote_account.activated_stake, + } + } +} + +#[derive(Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliNonceAccount { + pub balance: u64, + pub minimum_balance_for_rent_exemption: u64, + pub nonce: Option, + pub lamports_per_signature: Option, + pub authority: Option, + #[serde(skip_serializing)] + pub use_lamports_unit: bool, +} + +impl fmt::Display for CliNonceAccount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "Balance: {}", + build_balance_message(self.balance, self.use_lamports_unit, true) + )?; + writeln!( + f, + "Minimum Balance Required: {}", + build_balance_message( + self.minimum_balance_for_rent_exemption, + self.use_lamports_unit, + true + ) + )?; + let nonce = self.nonce.as_deref().unwrap_or("uninitialized"); + writeln!(f, "Nonce: {}", nonce)?; + if let Some(fees) = self.lamports_per_signature { + writeln!(f, "Fee: {} lamports per signature", fees)?; + } else { + writeln!(f, "Fees: uninitialized")?; + } + let authority = self.authority.as_deref().unwrap_or("uninitialized"); + writeln!(f, "Authority: {}", authority) + } +} + +#[derive(Serialize, Deserialize)] +pub struct CliStakeVec(Vec); + +impl CliStakeVec { + pub fn new(list: Vec) -> Self { + Self(list) + } +} + +impl fmt::Display for CliStakeVec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for state in &self.0 { + writeln!(f)?; + write!(f, "{}", state)?; + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliKeyedStakeState { + pub stake_pubkey: String, + #[serde(flatten)] + pub stake_state: CliStakeState, +} + +impl fmt::Display for CliKeyedStakeState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "Stake Pubkey: {}", self.stake_pubkey)?; + write!(f, "{}", self.stake_state) + } +} + +#[derive(Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliStakeState { + pub stake_type: CliStakeType, + pub total_stake: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub delegated_stake: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub delegated_vote_account_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub activation_epoch: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub deactivation_epoch: Option, + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub authorized: Option, + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub lockup: Option, + #[serde(skip_serializing)] + pub use_lamports_unit: bool, +} + +impl fmt::Display for CliStakeState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn show_authorized(f: &mut fmt::Formatter, authorized: &CliAuthorized) -> fmt::Result { + writeln!(f, "Stake Authority: {}", authorized.staker)?; + writeln!(f, "Withdraw Authority: {}", authorized.withdrawer)?; + Ok(()) + } + fn show_lockup(f: &mut fmt::Formatter, lockup: &CliLockup) -> fmt::Result { + writeln!( + f, + "Lockup Timestamp: {} (UnixTimestamp: {})", + DateTime::::from_utc( + NaiveDateTime::from_timestamp(lockup.unix_timestamp, 0), + Utc + ) + .to_rfc3339_opts(SecondsFormat::Secs, true), + lockup.unix_timestamp + )?; + writeln!(f, "Lockup Epoch: {}", lockup.epoch)?; + writeln!(f, "Lockup Custodian: {}", lockup.custodian)?; + Ok(()) + } + + match self.stake_type { + CliStakeType::RewardsPool => writeln!(f, "Stake account is a rewards pool")?, + CliStakeType::Uninitialized => writeln!(f, "Stake account is uninitialized")?, + CliStakeType::Initialized => { + writeln!( + f, + "Total Stake: {}", + build_balance_message(self.total_stake, self.use_lamports_unit, true) + )?; + writeln!(f, "Stake account is undelegated")?; + show_authorized(f, self.authorized.as_ref().unwrap())?; + show_lockup(f, self.lockup.as_ref().unwrap())?; + } + CliStakeType::Stake => { + writeln!( + f, + "Total Stake: {}", + build_balance_message(self.total_stake, self.use_lamports_unit, true) + )?; + writeln!( + f, + "Delegated Stake: {}", + build_balance_message( + self.delegated_stake.unwrap(), + self.use_lamports_unit, + true + ) + )?; + if let Some(delegated_vote_account_address) = &self.delegated_vote_account_address { + writeln!( + f, + "Delegated Vote Account Address: {}", + delegated_vote_account_address + )?; + } + writeln!( + f, + "Stake activates starting from epoch: {}", + self.activation_epoch.unwrap() + )?; + if let Some(deactivation_epoch) = self.deactivation_epoch { + writeln!( + f, + "Stake deactivates starting from epoch: {}", + deactivation_epoch + )?; + } + show_authorized(f, self.authorized.as_ref().unwrap())?; + show_lockup(f, self.lockup.as_ref().unwrap())?; + } + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +pub enum CliStakeType { + Stake, + RewardsPool, + Uninitialized, + Initialized, +} + +impl Default for CliStakeType { + fn default() -> Self { + Self::Uninitialized + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliStakeHistory { + pub entries: Vec, + #[serde(skip_serializing)] + pub use_lamports_unit: bool, +} + +impl fmt::Display for CliStakeHistory { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f)?; + writeln!( + f, + "{}", + style(format!( + " {:<5} {:>20} {:>20} {:>20}", + "Epoch", "Effective Stake", "Activating Stake", "Deactivating Stake", + )) + .bold() + )?; + for entry in &self.entries { + writeln!( + f, + " {:>5} {:>20} {:>20} {:>20} {}", + entry.epoch, + build_balance_message(entry.effective_stake, self.use_lamports_unit, false), + build_balance_message(entry.activating_stake, self.use_lamports_unit, false), + build_balance_message(entry.deactivating_stake, self.use_lamports_unit, false), + if self.use_lamports_unit { + "lamports" + } else { + "SOL" + } + )?; + } + Ok(()) + } +} + +impl From<&(Epoch, StakeHistoryEntry)> for CliStakeHistoryEntry { + fn from((epoch, entry): &(Epoch, StakeHistoryEntry)) -> Self { + Self { + epoch: *epoch, + effective_stake: entry.effective, + activating_stake: entry.activating, + deactivating_stake: entry.deactivating, + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliStakeHistoryEntry { + pub epoch: Epoch, + pub effective_stake: u64, + pub activating_stake: u64, + pub deactivating_stake: u64, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliAuthorized { + pub staker: String, + pub withdrawer: String, +} + +impl From<&Authorized> for CliAuthorized { + fn from(authorized: &Authorized) -> Self { + Self { + staker: authorized.staker.to_string(), + withdrawer: authorized.withdrawer.to_string(), + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliLockup { + pub unix_timestamp: UnixTimestamp, + pub epoch: Epoch, + pub custodian: String, +} + +impl From<&Lockup> for CliLockup { + fn from(lockup: &Lockup) -> Self { + Self { + unix_timestamp: lockup.unix_timestamp, + epoch: lockup.epoch, + custodian: lockup.custodian.to_string(), + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct CliValidatorInfoVec(Vec); + +impl CliValidatorInfoVec { + pub fn new(list: Vec) -> Self { + Self(list) + } +} + +impl fmt::Display for CliValidatorInfoVec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.0.is_empty() { + writeln!(f, "No validator info accounts found")?; + } + for validator_info in &self.0 { + writeln!(f)?; + write!(f, "{}", validator_info)?; + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliValidatorInfo { + pub identity_pubkey: String, + pub info_pubkey: String, + pub info: Map, +} + +impl fmt::Display for CliValidatorInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln_name_value(f, "Validator Identity Pubkey:", &self.identity_pubkey)?; + writeln_name_value(f, " Info Pubkey:", &self.info_pubkey)?; + for (key, value) in self.info.iter() { + writeln_name_value( + f, + &format!(" {}:", to_title_case(key)), + &value.as_str().unwrap_or("?"), + )?; + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliVoteAccount { + pub account_balance: u64, + pub validator_identity: String, + #[serde(flatten)] + pub authorized_voters: CliAuthorizedVoters, + pub authorized_withdrawer: String, + pub credits: u64, + pub commission: u8, + pub root_slot: Option, + pub recent_timestamp: BlockTimestamp, + pub votes: Vec, + pub epoch_voting_history: Vec, + #[serde(skip_serializing)] + pub use_lamports_unit: bool, +} + +impl fmt::Display for CliVoteAccount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "Account Balance: {}", + build_balance_message(self.account_balance, self.use_lamports_unit, true) + )?; + writeln!(f, "Validator Identity: {}", self.validator_identity)?; + writeln!(f, "Authorized Voters: {}", self.authorized_voters)?; + writeln!(f, "Authorized Withdrawer: {}", self.authorized_withdrawer)?; + writeln!(f, "Credits: {}", self.credits)?; + writeln!(f, "Commission: {}%", self.commission)?; + writeln!( + f, + "Root Slot: {}", + match self.root_slot { + Some(slot) => slot.to_string(), + None => "~".to_string(), + } + )?; + writeln!(f, "Recent Timestamp: {:?}", self.recent_timestamp)?; + if !self.votes.is_empty() { + writeln!(f, "Recent Votes:")?; + for vote in &self.votes { + writeln!( + f, + "- slot: {}\n confirmation count: {}", + vote.slot, vote.confirmation_count + )?; + } + writeln!(f, "Epoch Voting History:")?; + for epoch_info in &self.epoch_voting_history { + writeln!( + f, + "- epoch: {}\n slots in epoch: {}\n credits earned: {}", + epoch_info.epoch, epoch_info.slots_in_epoch, epoch_info.credits_earned, + )?; + } + } + Ok(()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliAuthorizedVoters { + authorized_voters: BTreeMap, +} + +impl fmt::Display for CliAuthorizedVoters { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self.authorized_voters) + } +} + +impl From<&AuthorizedVoters> for CliAuthorizedVoters { + fn from(authorized_voters: &AuthorizedVoters) -> Self { + let mut voter_map: BTreeMap = BTreeMap::new(); + for (epoch, voter) in authorized_voters.iter() { + voter_map.insert(*epoch, voter.to_string()); + } + Self { + authorized_voters: voter_map, + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliEpochVotingHistory { + pub epoch: Epoch, + pub slots_in_epoch: u64, + pub credits_earned: u64, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliLockout { + pub slot: Slot, + pub confirmation_count: u32, +} + +impl From<&Lockout> for CliLockout { + fn from(lockout: &Lockout) -> Self { + Self { + slot: lockout.slot, + confirmation_count: lockout.confirmation_count, + } + } +} diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 32aa2afce6..a1f793b232 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1,7 +1,8 @@ use crate::{ - cli::{ - build_balance_message, check_account_for_fee, CliCommand, CliCommandInfo, CliConfig, - CliError, ProcessResult, + cli::{check_account_for_fee, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, + cli_output::{ + CliBlockProduction, CliBlockProductionEntry, CliEpochInfo, CliKeyedStakeState, + CliSlotStatus, CliStakeVec, CliValidator, CliValidators, }, display::println_name_value, }; @@ -13,7 +14,6 @@ use solana_clap_utils::{input_parsers::*, input_validators::*, keypair::signer_f use solana_client::{ pubsub_client::{PubsubClient, SlotInfoMessage}, rpc_client::RpcClient, - rpc_response::RpcVoteAccountInfo, }; use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_sdk::{ @@ -42,7 +42,6 @@ use std::{ static CHECK_MARK: Emoji = Emoji("✅ ", ""); static CROSS_MARK: Emoji = Emoji("❌ ", ""); -static WARNING: Emoji = Emoji("⚠️", "!"); pub trait ClusterQuerySubCommands { fn cluster_query_subcommands(self) -> Self; @@ -599,51 +598,15 @@ pub fn process_get_block_time(rpc_client: &RpcClient, slot: Slot) -> ProcessResu Ok(result) } -fn slot_to_human_time(slot: Slot) -> String { - humantime::format_duration(Duration::from_secs( - slot * clock::DEFAULT_TICKS_PER_SLOT / clock::DEFAULT_TICKS_PER_SECOND, - )) - .to_string() -} - pub fn process_get_epoch_info( rpc_client: &RpcClient, + config: &CliConfig, commitment_config: CommitmentConfig, ) -> ProcessResult { - let epoch_info = rpc_client.get_epoch_info_with_commitment(commitment_config.clone())?; - println!(); - println_name_value("Slot:", &epoch_info.absolute_slot.to_string()); - println_name_value("Epoch:", &epoch_info.epoch.to_string()); - let start_slot = epoch_info.absolute_slot - epoch_info.slot_index; - let end_slot = start_slot + epoch_info.slots_in_epoch; - println_name_value( - "Epoch Slot Range:", - &format!("[{}..{})", start_slot, end_slot), - ); - println_name_value( - "Epoch Completed Percent:", - &format!( - "{:>3.3}%", - epoch_info.slot_index as f64 / epoch_info.slots_in_epoch as f64 * 100_f64 - ), - ); - let remaining_slots_in_epoch = epoch_info.slots_in_epoch - epoch_info.slot_index; - println_name_value( - "Epoch Completed Slots:", - &format!( - "{}/{} ({} remaining)", - epoch_info.slot_index, epoch_info.slots_in_epoch, remaining_slots_in_epoch - ), - ); - println_name_value( - "Epoch Completed Time:", - &format!( - "{}/{} ({} remaining)", - slot_to_human_time(epoch_info.slot_index), - slot_to_human_time(epoch_info.slots_in_epoch), - slot_to_human_time(remaining_slots_in_epoch) - ), - ); + let epoch_info: CliEpochInfo = rpc_client + .get_epoch_info_with_commitment(commitment_config.clone())? + .into(); + config.output_format.formatted_print(&epoch_info); Ok("".to_string()) } @@ -736,9 +699,9 @@ pub fn process_show_block_production( let start_slot_index = (start_slot - first_slot_in_epoch) as usize; let end_slot_index = (end_slot - first_slot_in_epoch) as usize; let total_slots = end_slot_index - start_slot_index + 1; - let total_blocks = confirmed_blocks.len(); - assert!(total_blocks <= total_slots); - let total_slots_skipped = total_slots - total_blocks; + let total_blocks_produced = confirmed_blocks.len(); + assert!(total_blocks_produced <= total_slots); + let total_slots_skipped = total_slots - total_blocks_produced; let mut leader_slot_count = HashMap::new(); let mut leader_skipped_slots = HashMap::new(); @@ -762,7 +725,7 @@ pub fn process_show_block_production( progress_bar.set_message(&format!( "Processing {} slots containing {} blocks and {} empty slots...", - total_slots, total_blocks, total_slots_skipped + total_slots, total_blocks_produced, total_slots_skipped )); let mut confirmed_blocks_index = 0; @@ -781,71 +744,52 @@ pub fn process_show_block_production( continue; } if slot_of_next_confirmed_block == slot { - individual_slot_status - .push(style(format!(" {:<15} {:<44}", slot, leader)).to_string()); + individual_slot_status.push(CliSlotStatus { + slot, + leader: (*leader).to_string(), + skipped: false, + }); break; } } *skipped_slots += 1; - individual_slot_status.push( - style(format!(" {:<15} {:<44} SKIPPED", slot, leader)) - .red() - .to_string(), - ); + individual_slot_status.push(CliSlotStatus { + slot, + leader: (*leader).to_string(), + skipped: true, + }); break; } } progress_bar.finish_and_clear(); - println!( - "\n{}", - style(format!( - " {:<44} {:>15} {:>15} {:>15} {:>23}", - "Identity Pubkey", - "Leader Slots", - "Blocks Produced", - "Skipped Slots", - "Skipped Slot Percentage", - )) - .bold() - ); - let mut table = vec![]; - for (leader, leader_slots) in leader_slot_count.iter() { - let skipped_slots = leader_skipped_slots.get(leader).unwrap(); - let blocks_produced = leader_slots - skipped_slots; - table.push(format!( - " {:<44} {:>15} {:>15} {:>15} {:>22.2}%", - leader, - leader_slots, - blocks_produced, - skipped_slots, - *skipped_slots as f64 / *leader_slots as f64 * 100. - )); - } - table.sort(); - - println!( - "{}\n\n {:<44} {:>15} {:>15} {:>15} {:>22.2}%", - table.join("\n"), - format!("Epoch {} total:", epoch), + let mut leaders: Vec = leader_slot_count + .iter() + .map(|(leader, leader_slots)| { + let skipped_slots = leader_skipped_slots.get(leader).unwrap(); + let blocks_produced = leader_slots - skipped_slots; + CliBlockProductionEntry { + identity_pubkey: (**leader).to_string(), + leader_slots: *leader_slots, + blocks_produced, + skipped_slots: *skipped_slots, + } + }) + .collect(); + leaders.sort_by(|a, b| a.identity_pubkey.partial_cmp(&b.identity_pubkey).unwrap()); + let block_production = CliBlockProduction { + epoch, + start_slot, + end_slot, total_slots, - total_blocks, + total_blocks_produced, total_slots_skipped, - total_slots_skipped as f64 / total_slots as f64 * 100. - ); - println!( - " (using data from {} slots: {} to {})", - total_slots, start_slot, end_slot - ); - - if config.verbose { - println!( - "\n\n{}\n{}", - style(format!(" {:<15} {:<44}", "Slot", "Identity Pubkey")).bold(), - individual_slot_status.join("\n") - ); - } + leaders, + individual_slot_status, + verbose: config.verbose, + }; + config.output_format.formatted_print(&block_production); Ok("".to_string()) } @@ -1127,10 +1071,11 @@ pub fn process_show_gossip(rpc_client: &RpcClient) -> ProcessResult { pub fn process_show_stakes( rpc_client: &RpcClient, + config: &CliConfig, use_lamports_unit: bool, vote_account_pubkeys: Option<&[Pubkey]>, ) -> ProcessResult { - use crate::stake::print_stake_state; + use crate::stake::build_stake_state; use solana_stake_program::stake_state::StakeState; let progress_bar = new_spinner_progress_bar(); @@ -1138,13 +1083,20 @@ pub fn process_show_stakes( let all_stake_accounts = rpc_client.get_program_accounts(&solana_stake_program::id())?; progress_bar.finish_and_clear(); + let mut stake_accounts: Vec = vec![]; 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); + stake_accounts.push(CliKeyedStakeState { + stake_pubkey: stake_pubkey.to_string(), + stake_state: build_stake_state( + stake_account.lamports, + &stake_state, + use_lamports_unit, + ), + }); } } StakeState::Stake(_, stake) => { @@ -1153,19 +1105,29 @@ pub fn process_show_stakes( .unwrap() .contains(&stake.delegation.voter_pubkey) { - println!("\nstake pubkey: {}", stake_pubkey); - print_stake_state(stake_account.lamports, &stake_state, use_lamports_unit); + stake_accounts.push(CliKeyedStakeState { + stake_pubkey: stake_pubkey.to_string(), + stake_state: build_stake_state( + stake_account.lamports, + &stake_state, + use_lamports_unit, + ), + }); } } _ => {} } } } + config + .output_format + .formatted_print(&CliStakeVec::new(stake_accounts)); Ok("".to_string()) } pub fn process_show_validators( rpc_client: &RpcClient, + config: &CliConfig, use_lamports_unit: bool, commitment_config: CommitmentConfig, ) -> ProcessResult { @@ -1175,126 +1137,36 @@ pub fn process_show_validators( .current .iter() .chain(vote_accounts.delinquent.iter()) - .fold(0, |acc, vote_account| acc + vote_account.activated_stake) - as f64; + .fold(0, |acc, vote_account| acc + vote_account.activated_stake); let total_deliquent_stake = vote_accounts .delinquent .iter() - .fold(0, |acc, vote_account| acc + vote_account.activated_stake) - as f64; + .fold(0, |acc, vote_account| acc + vote_account.activated_stake); let total_current_stake = total_active_stake - total_deliquent_stake; - println_name_value( - "Active Stake:", - &build_balance_message(total_active_stake as u64, use_lamports_unit, true), - ); - if total_deliquent_stake > 0. { - println_name_value( - "Current Stake:", - &format!( - "{} ({:0.2}%)", - &build_balance_message(total_current_stake as u64, use_lamports_unit, true), - 100. * total_current_stake / total_active_stake - ), - ); - println_name_value( - "Delinquent Stake:", - &format!( - "{} ({:0.2}%)", - &build_balance_message(total_deliquent_stake as u64, use_lamports_unit, true), - 100. * total_deliquent_stake / total_active_stake - ), - ); - } - println!(); - - println!( - "{}", - style(format!( - " {:<44} {:<44} {} {} {} {:>7} {}", - "Identity Pubkey", - "Vote Account Pubkey", - "Commission", - "Last Vote", - "Root Block", - "Credits", - "Active Stake", - )) - .bold() - ); - - fn print_vote_account( - vote_account: RpcVoteAccountInfo, - current_epoch: Epoch, - total_active_stake: f64, - use_lamports_unit: bool, - delinquent: bool, - ) { - fn non_zero_or_dash(v: u64) -> String { - if v == 0 { - "-".into() - } else { - format!("{}", v) - } - } - - println!( - "{} {:<44} {:<44} {:>9}% {:>8} {:>10} {:>7} {}", - if delinquent { - WARNING.to_string() - } else { - " ".to_string() - }, - vote_account.node_pubkey, - vote_account.vote_pubkey, - vote_account.commission, - non_zero_or_dash(vote_account.last_vote), - non_zero_or_dash(vote_account.root_slot), - vote_account - .epoch_credits - .iter() - .find_map(|(epoch, credits, _)| if *epoch == current_epoch { - Some(*credits) - } else { - None - }) - .unwrap_or(0), - if vote_account.activated_stake > 0 { - format!( - "{} ({:.2}%)", - build_balance_message(vote_account.activated_stake, use_lamports_unit, true), - 100. * vote_account.activated_stake as f64 / total_active_stake - ) - } else { - "-".into() - }, - ); - } - let mut current = vote_accounts.current; current.sort_by(|a, b| b.activated_stake.cmp(&a.activated_stake)); - for vote_account in current.into_iter() { - print_vote_account( - vote_account, - epoch_info.epoch, - total_active_stake, - use_lamports_unit, - false, - ); - } + let current_validators: Vec = current + .iter() + .map(|vote_account| CliValidator::new(vote_account, epoch_info.epoch)) + .collect(); let mut delinquent = vote_accounts.delinquent; delinquent.sort_by(|a, b| b.activated_stake.cmp(&a.activated_stake)); - for vote_account in delinquent.into_iter() { - print_vote_account( - vote_account, - epoch_info.epoch, - total_active_stake, - use_lamports_unit, - true, - ); - } + let delinquent_validators: Vec = delinquent + .iter() + .map(|vote_account| CliValidator::new(vote_account, epoch_info.epoch)) + .collect(); + let cli_validators = CliValidators { + total_active_stake, + total_current_stake, + total_deliquent_stake, + current_validators, + delinquent_validators, + use_lamports_unit, + }; + config.output_format.formatted_print(&cli_validators); Ok("".to_string()) } diff --git a/cli/src/display.rs b/cli/src/display.rs index 532eaddd1e..2fcc24ac7e 100644 --- a/cli/src/display.rs +++ b/cli/src/display.rs @@ -1,6 +1,7 @@ use crate::cli::SettingType; use console::style; use solana_sdk::hash::Hash; +use std::fmt; // Pretty print a "name value" pub fn println_name_value(name: &str, value: &str) { @@ -12,6 +13,15 @@ pub fn println_name_value(name: &str, value: &str) { println!("{} {}", style(name).bold(), styled_value); } +pub fn writeln_name_value(f: &mut fmt::Formatter, name: &str, value: &str) -> fmt::Result { + let styled_value = if value == "" { + style("(not set)").italic() + } else { + style(value) + }; + writeln!(f, "{} {}", style(name).bold(), styled_value) +} + pub fn println_name_value_or(name: &str, value: &str, setting_type: SettingType) { let description = match setting_type { SettingType::Explicit => "", diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 64b87e63c7..c67f8c48fd 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -18,7 +18,11 @@ macro_rules! pubkey { }; } +#[macro_use] +extern crate serde_derive; + pub mod cli; +pub mod cli_output; pub mod cluster_query; pub mod display; pub mod nonce; diff --git a/cli/src/main.rs b/cli/src/main.rs index b7be555357..cb3e71f5ea 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -9,6 +9,7 @@ use solana_clap_utils::{ }; use solana_cli::{ cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliSigners}, + cli_output::OutputFormat, display::{println_name_value, println_name_value_or}, }; use solana_cli_config::{Config, CONFIG_FILE}; @@ -129,6 +130,15 @@ pub fn parse_args<'a>( let CliCommandInfo { command, signers } = parse_command(&matches, &default_signer_path, wallet_manager.as_ref())?; + let output_format = matches + .value_of("output_format") + .map(|value| match value { + "json" => OutputFormat::Json, + "json-compact" => OutputFormat::JsonCompact, + _ => unreachable!(), + }) + .unwrap_or(OutputFormat::Display); + Ok(( CliConfig { command, @@ -138,6 +148,7 @@ pub fn parse_args<'a>( keypair_path: default_signer_path, rpc_client: None, verbose: matches.is_present("verbose"), + output_format, }, signers, )) @@ -199,6 +210,14 @@ fn main() -> Result<(), Box> { .global(true) .help("Show additional information"), ) + .arg( + Arg::with_name("output_format") + .long("output") + .global(true) + .takes_value(true) + .possible_values(&["json", "json-compact"]) + .help("Return information in specified output format. Supports: json, json-compact"), + ) .arg( Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name) .long(SKIP_SEED_PHRASE_VALIDATION_ARG.long) diff --git a/cli/src/nonce.rs b/cli/src/nonce.rs index 3fb70ed0ae..eceedc2662 100644 --- a/cli/src/nonce.rs +++ b/cli/src/nonce.rs @@ -1,7 +1,10 @@ -use crate::cli::{ - build_balance_message, check_account_for_fee, check_unique_pubkeys, generate_unique_signers, - log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, - SignerIndex, +use crate::{ + cli::{ + check_account_for_fee, check_unique_pubkeys, generate_unique_signers, + log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, + ProcessResult, SignerIndex, + }, + cli_output::CliNonceAccount, }; use clap::{App, Arg, ArgMatches, SubCommand}; use solana_clap_utils::{ @@ -568,38 +571,26 @@ pub fn process_new_nonce( pub fn process_show_nonce_account( rpc_client: &RpcClient, + config: &CliConfig, nonce_account_pubkey: &Pubkey, use_lamports_unit: bool, ) -> ProcessResult { let nonce_account = get_account(rpc_client, nonce_account_pubkey)?; let print_account = |data: Option<&nonce::state::Data>| { - println!( - "Balance: {}", - build_balance_message(nonce_account.lamports, use_lamports_unit, true) - ); - println!( - "Minimum Balance Required: {}", - build_balance_message( - rpc_client.get_minimum_balance_for_rent_exemption(State::size())?, - use_lamports_unit, - true - ) - ); - match data { - Some(ref data) => { - println!("Nonce: {}", data.blockhash); - println!( - "Fee: {} lamports per signature", - data.fee_calculator.lamports_per_signature - ); - println!("Authority: {}", data.authority); - } - None => { - println!("Nonce: uninitialized"); - println!("Fees: uninitialized"); - println!("Authority: uninitialized"); - } + let mut nonce_account = CliNonceAccount { + balance: nonce_account.lamports, + minimum_balance_for_rent_exemption: rpc_client + .get_minimum_balance_for_rent_exemption(State::size())?, + use_lamports_unit, + ..CliNonceAccount::default() + }; + if let Some(ref data) = data { + nonce_account.nonce = Some(data.blockhash.to_string()); + nonce_account.lamports_per_signature = Some(data.fee_calculator.lamports_per_signature); + nonce_account.authority = Some(data.authority.to_string()); } + + config.output_format.formatted_print(&nonce_account); Ok("".to_string()) }; match state_from_account(&nonce_account)? { diff --git a/cli/src/stake.rs b/cli/src/stake.rs index bcfdb80a50..c25db846c8 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -1,15 +1,14 @@ use crate::{ cli::{ - build_balance_message, check_account_for_fee, check_unique_pubkeys, fee_payer_arg, - generate_unique_signers, log_instruction_custom_error, nonce_authority_arg, return_signers, - CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, SignerIndex, FEE_PAYER_ARG, + check_account_for_fee, check_unique_pubkeys, fee_payer_arg, generate_unique_signers, + log_instruction_custom_error, nonce_authority_arg, return_signers, CliCommand, + CliCommandInfo, CliConfig, CliError, ProcessResult, SignerIndex, FEE_PAYER_ARG, }, + cli_output::{CliStakeHistory, CliStakeHistoryEntry, CliStakeState, CliStakeType}, nonce::{check_nonce_account, nonce_arg, NONCE_ARG, NONCE_AUTHORITY_ARG}, offline::{blockhash_query::BlockhashQuery, *}, }; -use chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc}; use clap::{App, Arg, ArgGroup, ArgMatches, SubCommand}; -use console::style; use solana_clap_utils::{input_parsers::*, input_validators::*, offline::*, ArgConstant}; use solana_client::rpc_client::RpcClient; use solana_remote_wallet::remote_wallet::RemoteWalletManager; @@ -1231,78 +1230,61 @@ pub fn process_stake_set_lockup( } } -pub fn print_stake_state(stake_lamports: u64, stake_state: &StakeState, use_lamports_unit: bool) { - fn show_authorized(authorized: &Authorized) { - println!("Stake Authority: {}", authorized.staker); - println!("Withdraw Authority: {}", authorized.withdrawer); - } - fn show_lockup(lockup: &Lockup) { - println!( - "Lockup Timestamp: {} (UnixTimestamp: {})", - DateTime::::from_utc(NaiveDateTime::from_timestamp(lockup.unix_timestamp, 0), Utc) - .to_rfc3339_opts(SecondsFormat::Secs, true), - lockup.unix_timestamp - ); - println!("Lockup Epoch: {}", lockup.epoch); - println!("Lockup Custodian: {}", lockup.custodian); - } +pub fn build_stake_state( + stake_lamports: u64, + stake_state: &StakeState, + use_lamports_unit: bool, +) -> CliStakeState { match stake_state { StakeState::Stake( Meta { authorized, lockup, .. }, stake, - ) => { - println!( - "Total Stake: {}", - build_balance_message(stake_lamports, use_lamports_unit, true) - ); - println!( - "Delegated Stake: {}", - build_balance_message(stake.delegation.stake, use_lamports_unit, true) - ); - if stake.delegation.voter_pubkey != Pubkey::default() { - println!( - "Delegated Vote Account Address: {}", - stake.delegation.voter_pubkey - ); - } - println!( - "Stake activates starting from epoch: {}", - if stake.delegation.activation_epoch < std::u64::MAX { - stake.delegation.activation_epoch - } else { - 0 - } - ); - if stake.delegation.deactivation_epoch < std::u64::MAX { - println!( - "Stake deactivates starting from epoch: {}", - stake.delegation.deactivation_epoch - ); - } - show_authorized(&authorized); - show_lockup(&lockup); - } - StakeState::RewardsPool => println!("Stake account is a rewards pool"), - StakeState::Uninitialized => println!("Stake account is uninitialized"), + ) => CliStakeState { + stake_type: CliStakeType::Stake, + total_stake: stake_lamports, + delegated_stake: Some(stake.delegation.stake), + delegated_vote_account_address: if stake.delegation.voter_pubkey != Pubkey::default() { + Some(stake.delegation.voter_pubkey.to_string()) + } else { + None + }, + activation_epoch: Some(if stake.delegation.activation_epoch < std::u64::MAX { + stake.delegation.activation_epoch + } else { + 0 + }), + deactivation_epoch: if stake.delegation.deactivation_epoch < std::u64::MAX { + Some(stake.delegation.deactivation_epoch) + } else { + None + }, + authorized: Some(authorized.into()), + lockup: Some(lockup.into()), + use_lamports_unit, + }, + StakeState::RewardsPool => CliStakeState { + stake_type: CliStakeType::RewardsPool, + ..CliStakeState::default() + }, + StakeState::Uninitialized => CliStakeState::default(), StakeState::Initialized(Meta { authorized, lockup, .. - }) => { - println!( - "Total Stake: {}", - build_balance_message(stake_lamports, use_lamports_unit, true) - ); - println!("Stake account is undelegated"); - show_authorized(&authorized); - show_lockup(&lockup); - } + }) => CliStakeState { + stake_type: CliStakeType::Initialized, + total_stake: stake_lamports, + authorized: Some(authorized.into()), + lockup: Some(lockup.into()), + use_lamports_unit, + ..CliStakeState::default() + }, } } pub fn process_show_stake_account( rpc_client: &RpcClient, - _config: &CliConfig, + config: &CliConfig, stake_account_pubkey: &Pubkey, use_lamports_unit: bool, ) -> ProcessResult { @@ -1316,7 +1298,8 @@ pub fn process_show_stake_account( } match stake_account.state() { Ok(stake_state) => { - print_stake_state(stake_account.lamports, &stake_state, use_lamports_unit); + let state = build_stake_state(stake_account.lamports, &stake_state, use_lamports_unit); + config.output_format.formatted_print(&state); Ok("".to_string()) } Err(err) => Err(CliError::RpcRequestError(format!( @@ -1329,7 +1312,7 @@ pub fn process_show_stake_account( pub fn process_show_stake_history( rpc_client: &RpcClient, - _config: &CliConfig, + config: &CliConfig, use_lamports_unit: bool, ) -> ProcessResult { let stake_history_account = rpc_client.get_account(&stake_history::id())?; @@ -1337,26 +1320,15 @@ pub fn process_show_stake_history( CliError::RpcRequestError("Failed to deserialize stake history".to_string()) })?; - println!(); - println!( - "{}", - style(format!( - " {:<5} {:>20} {:>20} {:>20}", - "Epoch", "Effective Stake", "Activating Stake", "Deactivating Stake", - )) - .bold() - ); - - for (epoch, entry) in stake_history.deref() { - println!( - " {:>5} {:>20} {:>20} {:>20} {}", - epoch, - build_balance_message(entry.effective, use_lamports_unit, false), - build_balance_message(entry.activating, use_lamports_unit, false), - build_balance_message(entry.deactivating, use_lamports_unit, false), - if use_lamports_unit { "lamports" } else { "SOL" } - ); + let mut entries: Vec = vec![]; + for entry in stake_history.deref() { + entries.push(entry.into()); } + let stake_history_output = CliStakeHistory { + entries, + use_lamports_unit, + }; + config.output_format.formatted_print(&stake_history_output); Ok("".to_string()) } diff --git a/cli/src/validator_info.rs b/cli/src/validator_info.rs index ba92767d5c..4678d78829 100644 --- a/cli/src/validator_info.rs +++ b/cli/src/validator_info.rs @@ -1,6 +1,6 @@ use crate::{ cli::{check_account_for_fee, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, - display::println_name_value, + cli_output::{CliValidatorInfo, CliValidatorInfoVec}, }; use bincode::deserialize; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; @@ -25,7 +25,6 @@ use solana_sdk::{ transaction::Transaction, }; use std::{error, sync::Arc}; -use titlecase::titlecase; pub const MAX_SHORT_FIELD_LENGTH: usize = 70; pub const MAX_LONG_FIELD_LENGTH: usize = 300; @@ -375,7 +374,11 @@ pub fn process_set_validator_info( Ok("".to_string()) } -pub fn process_get_validator_info(rpc_client: &RpcClient, pubkey: Option) -> ProcessResult { +pub fn process_get_validator_info( + rpc_client: &RpcClient, + config: &CliConfig, + pubkey: Option, +) -> ProcessResult { let validator_info: Vec<(Pubkey, Account)> = if let Some(validator_info_pubkey) = pubkey { vec![( validator_info_pubkey, @@ -394,23 +397,22 @@ pub fn process_get_validator_info(rpc_client: &RpcClient, pubkey: Option .collect() }; + let mut validator_info_list: Vec = vec![]; if validator_info.is_empty() { println!("No validator info accounts found"); } for (validator_info_pubkey, validator_info_account) in validator_info.iter() { let (validator_pubkey, validator_info) = parse_validator_info(&validator_info_pubkey, &validator_info_account)?; - println!(); - println_name_value("Validator Identity Pubkey:", &validator_pubkey.to_string()); - println_name_value(" Info Pubkey:", &validator_info_pubkey.to_string()); - for (key, value) in validator_info.iter() { - println_name_value( - &format!(" {}:", titlecase(key)), - &value.as_str().unwrap_or("?"), - ); - } + validator_info_list.push(CliValidatorInfo { + identity_pubkey: validator_pubkey.to_string(), + info_pubkey: validator_info_pubkey.to_string(), + info: validator_info, + }); } - + config + .output_format + .formatted_print(&CliValidatorInfoVec::new(validator_info_list)); Ok("".to_string()) } diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 92969a5304..8b284f62d1 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -1,7 +1,10 @@ -use crate::cli::{ - build_balance_message, check_account_for_fee, check_unique_pubkeys, generate_unique_signers, - log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult, - SignerIndex, +use crate::{ + cli::{ + check_account_for_fee, check_unique_pubkeys, generate_unique_signers, + log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, + ProcessResult, SignerIndex, + }, + cli_output::{CliEpochVotingHistory, CliLockout, CliVoteAccount}, }; use clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use solana_clap_utils::{input_parsers::*, input_validators::*}; @@ -549,7 +552,7 @@ fn get_vote_account( pub fn process_show_vote_account( rpc_client: &RpcClient, - _config: &CliConfig, + config: &CliConfig, vote_account_pubkey: &Pubkey, use_lamports_unit: bool, commitment_config: CommitmentConfig, @@ -559,45 +562,38 @@ pub fn process_show_vote_account( let epoch_schedule = rpc_client.get_epoch_schedule()?; - println!( - "Account Balance: {}", - build_balance_message(vote_account.lamports, use_lamports_unit, true) - ); - println!("Validator Identity: {}", vote_state.node_pubkey); - println!("Authorized Voter: {:?}", vote_state.authorized_voters()); - println!( - "Authorized Withdrawer: {}", - vote_state.authorized_withdrawer - ); - println!("Credits: {}", vote_state.credits()); - println!("Commission: {}%", vote_state.commission); - println!( - "Root Slot: {}", - match vote_state.root_slot { - Some(slot) => slot.to_string(), - None => "~".to_string(), - } - ); - println!("Recent Timestamp: {:?}", vote_state.last_timestamp); + let mut votes: Vec = vec![]; + let mut epoch_voting_history: Vec = vec![]; if !vote_state.votes.is_empty() { - println!("recent votes:"); for vote in &vote_state.votes { - println!( - "- slot: {}\n confirmation count: {}", - vote.slot, vote.confirmation_count - ); + votes.push(vote.into()); } - - println!("Epoch Voting History:"); for (epoch, credits, prev_credits) in vote_state.epoch_credits() { let credits_earned = credits - prev_credits; let slots_in_epoch = epoch_schedule.get_slots_in_epoch(*epoch); - println!( - "- epoch: {}\n slots in epoch: {}\n credits earned: {}", - epoch, slots_in_epoch, credits_earned, - ); + epoch_voting_history.push(CliEpochVotingHistory { + epoch: *epoch, + slots_in_epoch, + credits_earned, + }); } } + + let vote_account_data = CliVoteAccount { + account_balance: vote_account.lamports, + validator_identity: vote_state.node_pubkey.to_string(), + authorized_voters: vote_state.authorized_voters().into(), + authorized_withdrawer: vote_state.authorized_withdrawer.to_string(), + credits: vote_state.credits(), + commission: vote_state.commission, + root_slot: vote_state.root_slot, + recent_timestamp: vote_state.last_timestamp.clone(), + votes, + epoch_voting_history, + use_lamports_unit, + }; + + config.output_format.formatted_print(&vote_account_data); Ok("".to_string()) }