diff --git a/Cargo.lock b/Cargo.lock index fea6d8ac00..b1590c8bbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4639,6 +4639,7 @@ dependencies = [ "log 0.4.8", "serde_yaml", "solana-clap-utils", + "solana-cli", "solana-cli-config", "solana-client", "solana-logger", diff --git a/cli-config/src/config.rs b/cli-config/src/config.rs index e272970531..f766f1dec6 100644 --- a/cli-config/src/config.rs +++ b/cli-config/src/config.rs @@ -1,6 +1,6 @@ // Wallet settings that can be configured for long-term use use serde_derive::{Deserialize, Serialize}; -use std::io; +use std::{collections::HashMap, io, path::Path}; use url::Url; lazy_static! { @@ -17,6 +17,9 @@ pub struct Config { pub json_rpc_url: String, pub websocket_url: String, pub keypair_path: String, + + #[serde(default)] + pub address_labels: HashMap, } impl Default for Config { @@ -32,10 +35,17 @@ impl Default for Config { // `Config::compute_websocket_url(&json_rpc_url)` let websocket_url = "".to_string(); + let mut address_labels = HashMap::new(); + address_labels.insert( + "11111111111111111111111111111111".to_string(), + "System Program".to_string(), + ); + Self { json_rpc_url, websocket_url, keypair_path, + address_labels, } } } @@ -65,6 +75,24 @@ impl Config { } ws_url.to_string() } + + pub fn import_address_labels

(&mut self, filename: P) -> Result<(), io::Error> + where + P: AsRef, + { + let imports: HashMap = crate::load_config_file(filename)?; + for (address, label) in imports.into_iter() { + self.address_labels.insert(address, label); + } + Ok(()) + } + + pub fn export_address_labels

(&self, filename: P) -> Result<(), io::Error> + where + P: AsRef, + { + crate::save_config_file(&self.address_labels, filename) + } } #[cfg(test)] diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 6feee2aa88..23f8b53ecf 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -56,6 +56,7 @@ use solana_stake_program::{ use solana_transaction_status::{EncodedTransaction, TransactionEncoding}; use solana_vote_program::vote_state::VoteAuthorize; use std::{ + collections::HashMap, error, fmt::Write as FmtWrite, fs::File, @@ -493,6 +494,7 @@ pub struct CliConfig<'a> { pub output_format: OutputFormat, pub commitment: CommitmentConfig, pub send_transaction_config: RpcSendTransactionConfig, + pub address_labels: HashMap, } impl CliConfig<'_> { @@ -596,6 +598,7 @@ impl Default for CliConfig<'_> { output_format: OutputFormat::Display, commitment: CommitmentConfig::default(), send_transaction_config: RpcSendTransactionConfig::default(), + address_labels: HashMap::new(), } } } @@ -1837,7 +1840,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { CliCommand::ShowBlockProduction { epoch, slot_limit } => { process_show_block_production(&rpc_client, config, *epoch, *slot_limit) } - CliCommand::ShowGossip => process_show_gossip(&rpc_client), + CliCommand::ShowGossip => process_show_gossip(&rpc_client, config), CliCommand::ShowStakes { use_lamports_unit, vote_account_pubkeys, diff --git a/cli/src/cli_output.rs b/cli/src/cli_output.rs index fa7a4cb767..aba78a5085 100644 --- a/cli/src/cli_output.rs +++ b/cli/src/cli_output.rs @@ -1,4 +1,7 @@ -use crate::{cli::build_balance_message, display::writeln_name_value}; +use crate::{ + cli::build_balance_message, + display::{format_labeled_address, writeln_name_value}, +}; use chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc}; use console::{style, Emoji}; use inflector::cases::titlecase::to_title_case; @@ -18,7 +21,11 @@ use solana_vote_program::{ authorized_voters::AuthorizedVoters, vote_state::{BlockTimestamp, Lockout}, }; -use std::{collections::BTreeMap, fmt, time::Duration}; +use std::{ + collections::{BTreeMap, HashMap}, + fmt, + time::Duration, +}; static WARNING: Emoji = Emoji("⚠️", "!"); @@ -404,10 +411,15 @@ pub struct CliValidator { } impl CliValidator { - pub fn new(vote_account: &RpcVoteAccountInfo, current_epoch: Epoch, version: String) -> Self { + pub fn new( + vote_account: &RpcVoteAccountInfo, + current_epoch: Epoch, + version: String, + address_labels: &HashMap, + ) -> Self { Self { - identity_pubkey: vote_account.node_pubkey.to_string(), - vote_account_pubkey: vote_account.vote_pubkey.to_string(), + identity_pubkey: format_labeled_address(&vote_account.node_pubkey, address_labels), + vote_account_pubkey: format_labeled_address(&vote_account.vote_pubkey, address_labels), commission: vote_account.commission, last_vote: vote_account.last_vote, root_slot: vote_account.root_slot, diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index e9669d22aa..403f75ade2 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1,7 +1,7 @@ use crate::{ cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, cli_output::*, - display::{new_spinner_progress_bar, println_name_value}, + display::{format_labeled_address, new_spinner_progress_bar, println_name_value}, spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount}, }; use clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; @@ -734,11 +734,12 @@ pub fn process_show_block_production( let leader_schedule = leader_schedule.unwrap(); let mut leader_per_slot_index = Vec::new(); - leader_per_slot_index.resize(total_slots, "?"); + leader_per_slot_index.resize(total_slots, "?".to_string()); for (pubkey, leader_slots) in leader_schedule.iter() { + let pubkey = format_labeled_address(pubkey, &config.address_labels); for slot_index in leader_slots.iter() { if *slot_index >= start_slot_index && *slot_index <= end_slot_index { - leader_per_slot_index[*slot_index - start_slot_index] = pubkey; + leader_per_slot_index[*slot_index - start_slot_index] = pubkey.clone(); } } } @@ -1085,7 +1086,7 @@ pub fn process_live_slots(url: &str) -> ProcessResult { Ok("".to_string()) } -pub fn process_show_gossip(rpc_client: &RpcClient) -> ProcessResult { +pub fn process_show_gossip(rpc_client: &RpcClient, config: &CliConfig) -> ProcessResult { let cluster_nodes = rpc_client.get_cluster_nodes()?; fn format_port(addr: Option) -> String { @@ -1101,7 +1102,7 @@ pub fn process_show_gossip(rpc_client: &RpcClient) -> ProcessResult { node.gossip .map(|addr| addr.ip().to_string()) .unwrap_or_else(|| "none".to_string()), - node.pubkey, + format_labeled_address(&node.pubkey, &config.address_labels), format_port(node.gossip), format_port(node.tpu), format_port(node.rpc), @@ -1235,6 +1236,7 @@ pub fn process_show_validators( .get(&vote_account.node_pubkey) .unwrap_or(&unknown_version) .clone(), + &config.address_labels, ) }) .collect(); @@ -1250,6 +1252,7 @@ pub fn process_show_validators( .get(&vote_account.node_pubkey) .unwrap_or(&unknown_version) .clone(), + &config.address_labels, ) }) .collect(); diff --git a/cli/src/display.rs b/cli/src/display.rs index b089564b17..091c400ab2 100644 --- a/cli/src/display.rs +++ b/cli/src/display.rs @@ -6,7 +6,7 @@ use solana_sdk::{ transaction::Transaction, }; use solana_transaction_status::RpcTransactionStatusMeta; -use std::{fmt, io}; +use std::{collections::HashMap, fmt, io}; // Pretty print a "name value" pub fn println_name_value(name: &str, value: &str) { @@ -27,6 +27,19 @@ pub fn writeln_name_value(f: &mut fmt::Formatter, name: &str, value: &str) -> fm writeln!(f, "{} {}", style(name).bold(), styled_value) } +pub fn format_labeled_address(pubkey: &str, address_labels: &HashMap) -> String { + let label = address_labels.get(pubkey); + match label { + Some(label) => format!( + "{:.31} ({:.4}..{})", + label, + pubkey, + pubkey.split_at(pubkey.len() - 4).1 + ), + None => pubkey.to_string(), + } +} + pub fn println_name_value_or(name: &str, value: &str, setting_type: SettingType) { let description = match setting_type { SettingType::Explicit => "", @@ -210,3 +223,32 @@ pub fn new_spinner_progress_bar() -> ProgressBar { progress_bar.enable_steady_tick(100); progress_bar } + +#[cfg(test)] +mod test { + use super::*; + use solana_sdk::pubkey::Pubkey; + + #[test] + fn test_format_labeled_address() { + let pubkey = Pubkey::default().to_string(); + let mut address_labels = HashMap::new(); + + assert_eq!(format_labeled_address(&pubkey, &address_labels), pubkey); + + address_labels.insert(pubkey.to_string(), "Default Address".to_string()); + assert_eq!( + &format_labeled_address(&pubkey, &address_labels), + "Default Address (1111..1111)" + ); + + address_labels.insert( + pubkey.to_string(), + "abcdefghijklmnopqrstuvwxyz1234567890".to_string(), + ); + assert_eq!( + &format_labeled_address(&pubkey, &address_labels), + "abcdefghijklmnopqrstuvwxyz12345 (1111..1111)" + ); + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs index f889636a8d..d2006754a1 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,4 +1,7 @@ -use clap::{crate_description, crate_name, AppSettings, Arg, ArgGroup, ArgMatches, SubCommand}; +use clap::{ + crate_description, crate_name, value_t_or_exit, AppSettings, Arg, ArgGroup, ArgMatches, + SubCommand, +}; use console::style; use solana_clap_utils::{ @@ -13,15 +16,25 @@ use solana_cli::{ use solana_cli_config::{Config, CONFIG_FILE}; use solana_client::rpc_config::RpcSendTransactionConfig; use solana_remote_wallet::remote_wallet::RemoteWalletManager; -use std::{error, sync::Arc}; +use std::{collections::HashMap, error, path::PathBuf, sync::Arc}; fn parse_settings(matches: &ArgMatches<'_>) -> Result> { let parse_args = match matches.subcommand() { - ("config", Some(matches)) => match matches.subcommand() { - ("get", Some(subcommand_matches)) => { - if let Some(config_file) = matches.value_of("config_file") { - let config = Config::load(config_file).unwrap_or_default(); + ("config", Some(matches)) => { + let config_file = match matches.value_of("config_file") { + None => { + println!( + "{} Either provide the `--config` arg or ensure home directory exists to use the default config location", + style("No config file found.").bold() + ); + return Ok(false); + } + Some(config_file) => config_file, + }; + let mut config = Config::load(config_file).unwrap_or_default(); + match matches.subcommand() { + ("get", Some(subcommand_matches)) => { let (url_setting_type, json_rpc_url) = CliConfig::compute_json_rpc_url_setting("", &config.json_rpc_url); let (ws_setting_type, websocket_url) = CliConfig::compute_websocket_url_setting( @@ -47,17 +60,8 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result { - if let Some(config_file) = matches.value_of("config_file") { - let mut config = Config::load(config_file).unwrap_or_default(); + ("set", Some(subcommand_matches)) => { if let Some(url) = subcommand_matches.value_of("json_rpc_url") { config.json_rpc_url = url.to_string(); // Revert to a computed `websocket_url` value when `json_rpc_url` is @@ -70,6 +74,7 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result) -> Result { + let filename = value_t_or_exit!(subcommand_matches, "filename", PathBuf); + config.import_address_labels(&filename)?; + config.save(config_file)?; + println!("Address labels imported from {:?}", filename); + } + ("export-address-labels", Some(subcommand_matches)) => { + let filename = value_t_or_exit!(subcommand_matches, "filename", PathBuf); + config.export_address_labels(&filename)?; + println!("Address labels exported to {:?}", filename); + } + _ => unreachable!(), } - _ => unreachable!(), - }, + false + } _ => true, }; Ok(parse_args) @@ -144,6 +155,12 @@ pub fn parse_args<'a>( .and_then(|sub_matches| commitment_of(sub_matches, COMMITMENT_ARG.long)) .unwrap_or_default(); + let address_labels = if matches.is_present("no_address_labels") { + HashMap::new() + } else { + config.address_labels + }; + Ok(( CliConfig { command, @@ -156,6 +173,7 @@ pub fn parse_args<'a>( output_format, commitment, send_transaction_config: RpcSendTransactionConfig::default(), + address_labels, }, signers, )) @@ -217,6 +235,12 @@ fn main() -> Result<(), Box> { .global(true) .help("Show additional information"), ) + .arg( + Arg::with_name("no_address_labels") + .long("no-address-labels") + .global(true) + .help("Do not use address labels in the output"), + ) .arg( Arg::with_name("output_format") .long("output") @@ -258,6 +282,28 @@ fn main() -> Result<(), Box> { .multiple(true) .required(true), ), + ) + .subcommand( + SubCommand::with_name("import-address-labels") + .about("Import a list of address labels") + .arg( + Arg::with_name("filename") + .index(1) + .value_name("FILENAME") + .takes_value(true) + .help("YAML file of address labels"), + ), + ) + .subcommand( + SubCommand::with_name("export-address-labels") + .about("Export the current address labels") + .arg( + Arg::with_name("filename") + .index(1) + .value_name("FILENAME") + .takes_value(true) + .help("YAML file to receive the current address labels"), + ), ), ) .get_matches(); diff --git a/stake-o-matic/Cargo.toml b/stake-o-matic/Cargo.toml index 19ba855051..d625f6a45e 100644 --- a/stake-o-matic/Cargo.toml +++ b/stake-o-matic/Cargo.toml @@ -15,6 +15,7 @@ serde_yaml = "0.8.13" solana-clap-utils = { path = "../clap-utils", version = "1.3.0" } solana-client = { path = "../client", version = "1.3.0" } solana-cli-config = { path = "../cli-config", version = "1.3.0" } +solana-cli = { path = "../cli", version = "1.3.0" } solana-logger = { path = "../logger", version = "1.3.0" } solana-metrics = { path = "../metrics", version = "1.3.0" } solana-notifier = { path = "../notifier", version = "1.3.0" } diff --git a/stake-o-matic/src/main.rs b/stake-o-matic/src/main.rs index 87e11df974..261806e797 100644 --- a/stake-o-matic/src/main.rs +++ b/stake-o-matic/src/main.rs @@ -4,6 +4,7 @@ use solana_clap_utils::{ input_parsers::{keypair_of, pubkey_of}, input_validators::{is_amount, is_keypair, is_pubkey_or_keypair, is_url, is_valid_percentage}, }; +use solana_cli::display::format_labeled_address; use solana_client::{ client_error, rpc_client::RpcClient, rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, rpc_response::RpcVoteAccountInfo, @@ -65,6 +66,8 @@ struct Config { /// Don't ever unstake more than this percentage of the cluster at one time max_poor_block_productor_percentage: usize, + + address_labels: HashMap, } fn get_config() -> Config { @@ -208,7 +211,8 @@ fn get_config() -> Config { }) .collect(); ( - value_t!(matches, "json_rpc_url", String).unwrap_or_else(|_| config.json_rpc_url), + value_t!(matches, "json_rpc_url", String) + .unwrap_or_else(|_| config.json_rpc_url.clone()), validator_list, ) } @@ -228,6 +232,7 @@ fn get_config() -> Config { delinquent_grace_slot_distance: 21600, // ~24 hours worth of slots at 2.5 slots per second quality_block_producer_percentage, max_poor_block_productor_percentage: 20, + address_labels: config.address_labels, }; info!("RPC URL: {}", config.json_rpc_url); @@ -596,6 +601,7 @@ fn main() -> Result<(), Box> { .. } in &vote_account_info { + let formatted_node_pubkey = format_labeled_address(&node_pubkey, &config.address_labels); let node_pubkey = Pubkey::from_str(&node_pubkey).unwrap(); let baseline_seed = &vote_pubkey.to_string()[..32]; let bonus_seed = &format!("A{{{}", vote_pubkey)[..32]; @@ -632,7 +638,7 @@ fn main() -> Result<(), Box> { } else { info!( "Need to create baseline stake account for validator {}", - node_pubkey + formatted_node_pubkey ); source_stake_lamports_required += config.baseline_stake_amount; create_stake_transactions.push(( @@ -649,7 +655,7 @@ fn main() -> Result<(), Box> { )), format!( "Creating baseline stake account for validator {} ({})", - node_pubkey, baseline_stake_address + formatted_node_pubkey, baseline_stake_address ), )); } @@ -670,7 +676,7 @@ fn main() -> Result<(), Box> { } else { info!( "Need to create bonus stake account for validator {}", - node_pubkey + formatted_node_pubkey ); source_stake_lamports_required += config.bonus_stake_amount; create_stake_transactions.push(( @@ -687,7 +693,7 @@ fn main() -> Result<(), Box> { )), format!( "Creating bonus stake account for validator {} ({})", - node_pubkey, bonus_stake_address + formatted_node_pubkey, bonus_stake_address ), )); } @@ -716,7 +722,7 @@ fn main() -> Result<(), Box> { )), format!( "🥩 `{}` is current. Added ◎{} baseline stake", - node_pubkey, + formatted_node_pubkey, lamports_to_sol(config.baseline_stake_amount), ), )); @@ -738,7 +744,7 @@ fn main() -> Result<(), Box> { )), format!( "🏅 `{}` was a quality block producer during epoch {}. Added ◎{} bonus stake", - node_pubkey, + formatted_node_pubkey, last_epoch, lamports_to_sol(config.bonus_stake_amount), ), @@ -757,7 +763,7 @@ fn main() -> Result<(), Box> { )), format!( "💔 `{}` was a poor block producer during epoch {}. Removed ◎{} bonus stake", - node_pubkey, + formatted_node_pubkey, last_epoch, lamports_to_sol(config.bonus_stake_amount), ), @@ -782,7 +788,7 @@ fn main() -> Result<(), Box> { )), format!( "🏖️ `{}` is delinquent. Removed ◎{} baseline stake", - node_pubkey, + formatted_node_pubkey, lamports_to_sol(config.baseline_stake_amount), ), )); @@ -798,7 +804,7 @@ fn main() -> Result<(), Box> { )), format!( "🏖️ `{}` is delinquent. Removed ◎{} bonus stake", - node_pubkey, + formatted_node_pubkey, lamports_to_sol(config.bonus_stake_amount), ), )); diff --git a/watchtower/src/main.rs b/watchtower/src/main.rs index d9c54d250b..4a69fc2087 100644 --- a/watchtower/src/main.rs +++ b/watchtower/src/main.rs @@ -6,6 +6,7 @@ use solana_clap_utils::{ input_parsers::pubkeys_of, input_validators::{is_pubkey_or_keypair, is_url}, }; +use solana_cli::display::{format_labeled_address, write_transaction}; use solana_client::{ client_error::Result as ClientResult, rpc_client::RpcClient, rpc_response::RpcVoteAccountStatus, }; @@ -18,6 +19,7 @@ use solana_sdk::{ use solana_transaction_status::{ConfirmedBlock, TransactionEncoding}; use solana_vote_program::vote_instruction::VoteInstruction; use std::{ + collections::HashMap, error, str::FromStr, thread::sleep, @@ -31,6 +33,7 @@ struct Config { no_duplicate_notifications: bool, monitor_active_stake: bool, notify_on_transactions: bool, + address_labels: HashMap, } fn get_config() -> Config { @@ -123,7 +126,7 @@ fn get_config() -> Config { let interval = Duration::from_secs(value_t_or_exit!(matches, "interval", u64)); let json_rpc_url = - value_t!(matches, "json_rpc_url", String).unwrap_or_else(|_| config.json_rpc_url); + value_t!(matches, "json_rpc_url", String).unwrap_or_else(|_| config.json_rpc_url.clone()); let validator_identity_pubkeys: Vec<_> = pubkeys_of(&matches, "validator_identities") .unwrap_or_else(Vec::new) .into_iter() @@ -141,6 +144,7 @@ fn get_config() -> Config { no_duplicate_notifications, monitor_active_stake, notify_on_transactions, + address_labels: config.address_labels, }; info!("RPC URL: {}", config.json_rpc_url); @@ -184,14 +188,7 @@ fn process_confirmed_block(notifier: &Notifier, slot: Slot, confirmed_block: Con if notify { let mut w = Vec::new(); - if solana_cli::display::write_transaction( - &mut w, - &transaction, - &rpc_transaction.meta, - "", - ) - .is_ok() - { + if write_transaction(&mut w, &transaction, &rpc_transaction.meta, "").is_ok() { if let Ok(s) = String::from_utf8(w) { notifier.send(&format!("```Slot: {}\n{}```", slot, s)); } @@ -385,18 +382,20 @@ fn main() -> Result<(), Box> { } else { let mut errors = vec![]; for validator_identity in config.validator_identity_pubkeys.iter() { + let formatted_validator_identity = + format_labeled_address(&validator_identity, &config.address_labels); if vote_accounts .delinquent .iter() .any(|vai| vai.node_pubkey == *validator_identity) { - errors.push(format!("{} delinquent", validator_identity)); + errors.push(format!("{} delinquent", formatted_validator_identity)); } else if !vote_accounts .current .iter() .any(|vai| vai.node_pubkey == *validator_identity) { - errors.push(format!("{} missing", validator_identity)); + errors.push(format!("{} missing", formatted_validator_identity)); } rpc_client @@ -408,12 +407,18 @@ fn main() -> Result<(), Box> { // find some more SOL failures.push(( "balance", - format!("{} has {} SOL", validator_identity, balance), + format!( + "{} has {} SOL", + formatted_validator_identity, balance + ), )); } }) .unwrap_or_else(|err| { - warn!("Failed to get balance of {}: {:?}", validator_identity, err); + warn!( + "Failed to get balance of {}: {:?}", + formatted_validator_identity, err + ); }); }