Permit users to assign labels to account addresses

This commit is contained in:
Michael Vines 2020-06-17 23:09:33 -07:00 committed by mergify[bot]
parent a25ea8e774
commit b297d0b423
10 changed files with 207 additions and 60 deletions

1
Cargo.lock generated
View File

@ -4639,6 +4639,7 @@ dependencies = [
"log 0.4.8",
"serde_yaml",
"solana-clap-utils",
"solana-cli",
"solana-cli-config",
"solana-client",
"solana-logger",

View File

@ -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<String, String>,
}
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<P>(&mut self, filename: P) -> Result<(), io::Error>
where
P: AsRef<Path>,
{
let imports: HashMap<String, String> = crate::load_config_file(filename)?;
for (address, label) in imports.into_iter() {
self.address_labels.insert(address, label);
}
Ok(())
}
pub fn export_address_labels<P>(&self, filename: P) -> Result<(), io::Error>
where
P: AsRef<Path>,
{
crate::save_config_file(&self.address_labels, filename)
}
}
#[cfg(test)]

View File

@ -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<String, String>,
}
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,

View File

@ -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<String, String>,
) -> 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,

View File

@ -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<SocketAddr>) -> 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();

View File

@ -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, String>) -> 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)"
);
}
}

View File

@ -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<bool, Box<dyn error::Error>> {
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<bool, Box<dyn error::Error
println_name_value_or("WebSocket URL:", &websocket_url, ws_setting_type);
println_name_value_or("Keypair Path:", &keypair_path, keypair_setting_type);
}
} else {
println!(
"{} Either provide the `--config` arg or ensure home directory exists to use the default config location",
style("No config file found.").bold()
);
}
false
}
("set", Some(subcommand_matches)) => {
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<bool, Box<dyn error::Error
if let Some(keypair) = subcommand_matches.value_of("keypair") {
config.keypair_path = keypair.to_string();
}
config.save(config_file)?;
let (url_setting_type, json_rpc_url) =
@ -87,16 +92,22 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error
println_name_value_or("RPC URL:", &json_rpc_url, url_setting_type);
println_name_value_or("WebSocket URL:", &websocket_url, ws_setting_type);
println_name_value_or("Keypair Path:", &keypair_path, keypair_setting_type);
} else {
println!(
"{} Either provide the `--config` arg or ensure home directory exists to use the default config location",
style("No config file found.").bold()
);
}
false
("import-address-labels", Some(subcommand_matches)) => {
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<dyn error::Error>> {
.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<dyn error::Error>> {
.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();

View File

@ -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" }

View File

@ -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<String, String>,
}
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<dyn error::Error>> {
..
} 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<dyn error::Error>> {
} 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<dyn error::Error>> {
)),
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<dyn error::Error>> {
} 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<dyn error::Error>> {
)),
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<dyn error::Error>> {
)),
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<dyn error::Error>> {
)),
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<dyn error::Error>> {
)),
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<dyn error::Error>> {
)),
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<dyn error::Error>> {
)),
format!(
"🏖️ `{}` is delinquent. Removed ◎{} bonus stake",
node_pubkey,
formatted_node_pubkey,
lamports_to_sol(config.bonus_stake_amount),
),
));

View File

@ -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<String, String>,
}
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<dyn error::Error>> {
} 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<dyn error::Error>> {
// 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
);
});
}