diff --git a/cli/src/input_parsers.rs b/cli/src/input_parsers.rs new file mode 100644 index 0000000000..47cf3e85a7 --- /dev/null +++ b/cli/src/input_parsers.rs @@ -0,0 +1,157 @@ +use clap::ArgMatches; +use solana_sdk::{ + pubkey::Pubkey, + signature::{read_keypair, Keypair, KeypairUtil}, +}; + +// Return parsed values from matches at `name` +pub fn values_of(matches: &ArgMatches<'_>, name: &str) -> Option> +where + T: std::str::FromStr, + ::Err: std::fmt::Debug, +{ + matches + .values_of(name) + .map(|xs| xs.map(|x| x.parse::().unwrap()).collect()) +} + +// Return a parsed value from matches at `name` +pub fn value_of(matches: &ArgMatches<'_>, name: &str) -> Option +where + T: std::str::FromStr, + ::Err: std::fmt::Debug, +{ + if let Some(value) = matches.value_of(name) { + value.parse::().ok() + } else { + None + } +} + +// Return the keypair for an argument with filename `name` or None if not present. +pub fn keypair_of(matches: &ArgMatches<'_>, name: &str) -> Option { + if let Some(value) = matches.value_of(name) { + read_keypair(value).ok() + } else { + None + } +} + +// Return a pubkey for an argument that can itself be parsed into a pubkey, +// or is a filename that can be read as a keypair +pub fn pubkey_of(matches: &ArgMatches<'_>, name: &str) -> Option { + value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey())) +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::{App, Arg}; + use solana_sdk::signature::write_keypair; + use std::fs; + + fn app<'ab, 'v>() -> App<'ab, 'v> { + App::new("test") + .arg( + Arg::with_name("multiple") + .long("multiple") + .takes_value(true) + .multiple(true), + ) + .arg(Arg::with_name("single").takes_value(true).long("single")) + } + + fn tmp_file_path(name: &str, pubkey: &Pubkey) -> String { + use std::env; + let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); + + format!("{}/tmp/{}-{}", out_dir, name, pubkey.to_string()) + } + + #[test] + fn test_values_of() { + let matches = + app() + .clone() + .get_matches_from(vec!["test", "--multiple", "50", "--multiple", "39"]); + assert_eq!(values_of(&matches, "multiple"), Some(vec![50, 39])); + assert_eq!(values_of::(&matches, "single"), None); + + let pubkey0 = Pubkey::new_rand(); + let pubkey1 = Pubkey::new_rand(); + let matches = app().clone().get_matches_from(vec![ + "test", + "--multiple", + &pubkey0.to_string(), + "--multiple", + &pubkey1.to_string(), + ]); + assert_eq!( + values_of(&matches, "multiple"), + Some(vec![pubkey0, pubkey1]) + ); + } + + #[test] + fn test_value_of() { + let matches = app() + .clone() + .get_matches_from(vec!["test", "--single", "50"]); + assert_eq!(value_of(&matches, "single"), Some(50)); + assert_eq!(value_of::(&matches, "multiple"), None); + + let pubkey = Pubkey::new_rand(); + let matches = app() + .clone() + .get_matches_from(vec!["test", "--single", &pubkey.to_string()]); + assert_eq!(value_of(&matches, "single"), Some(pubkey)); + } + + #[test] + fn test_keypair_of() { + let keypair = Keypair::new(); + let outfile = tmp_file_path("test_gen_keypair_file.json", &keypair.pubkey()); + let _ = write_keypair(&keypair, &outfile).unwrap(); + + let matches = app() + .clone() + .get_matches_from(vec!["test", "--single", &outfile]); + assert_eq!(keypair_of(&matches, "single"), Some(keypair)); + assert_eq!(keypair_of(&matches, "multiple"), None); + + let matches = + app() + .clone() + .get_matches_from(vec!["test", "--single", "random_keypair_file.json"]); + assert_eq!(keypair_of(&matches, "single"), None); + + fs::remove_file(&outfile).unwrap(); + } + + #[test] + fn test_pubkey_of() { + let keypair = Keypair::new(); + let outfile = tmp_file_path("test_gen_keypair_file.json", &keypair.pubkey()); + let _ = write_keypair(&keypair, &outfile).unwrap(); + + let matches = app() + .clone() + .get_matches_from(vec!["test", "--single", &outfile]); + assert_eq!(pubkey_of(&matches, "single"), Some(keypair.pubkey())); + assert_eq!(pubkey_of(&matches, "multiple"), None); + + let matches = + app() + .clone() + .get_matches_from(vec!["test", "--single", &keypair.pubkey().to_string()]); + assert_eq!(pubkey_of(&matches, "single"), Some(keypair.pubkey())); + + let matches = + app() + .clone() + .get_matches_from(vec!["test", "--single", "random_keypair_file.json"]); + assert_eq!(pubkey_of(&matches, "single"), None); + + fs::remove_file(&outfile).unwrap(); + } +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index e26b513d75..9db1458a29 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -3,8 +3,10 @@ extern crate lazy_static; pub mod config; pub mod display; +pub mod input_parsers; pub mod input_validators; pub mod validator_info; +pub mod vote; pub mod wallet; pub(crate) fn lamports_to_sol(lamports: u64) -> f64 { diff --git a/cli/src/vote.rs b/cli/src/vote.rs new file mode 100644 index 0000000000..0edff51968 --- /dev/null +++ b/cli/src/vote.rs @@ -0,0 +1,353 @@ +use crate::{ + input_parsers::*, + wallet::{ + check_account_for_fee, check_unique_pubkeys, log_instruction_custom_error, ProcessResult, + WalletCommand, WalletConfig, WalletError, + }, +}; +use clap::{value_t_or_exit, ArgMatches}; +use solana_client::rpc_client::RpcClient; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, KeypairUtil}, + system_instruction::SystemError, + transaction::Transaction, +}; +use solana_vote_api::{ + vote_instruction::{self, VoteError}, + vote_state::VoteState, +}; + +pub fn parse_vote_create_account(matches: &ArgMatches<'_>) -> Result { + let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); + let node_pubkey = pubkey_of(matches, "node_pubkey").unwrap(); + let commission = value_of(&matches, "commission").unwrap_or(0); + let lamports = matches + .value_of("lamports") + .unwrap() + .parse() + .map_err(|err| WalletError::BadParameter(format!("Invalid lamports: {:?}", err)))?; + Ok(WalletCommand::CreateVoteAccount( + vote_account_pubkey, + node_pubkey, + commission, + lamports, + )) +} + +pub fn parse_vote_authorize_voter(matches: &ArgMatches<'_>) -> Result { + let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); + let authorized_voter_keypair = keypair_of(matches, "authorized_voter_keypair_file").unwrap(); + let new_authorized_voter_pubkey = pubkey_of(matches, "new_authorized_voter_pubkey").unwrap(); + + Ok(WalletCommand::AuthorizeVoter( + vote_account_pubkey, + authorized_voter_keypair, + new_authorized_voter_pubkey, + )) +} + +pub fn parse_vote_get_account_command( + matches: &ArgMatches<'_>, +) -> Result { + let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); + Ok(WalletCommand::ShowVoteAccount(vote_account_pubkey)) +} + +pub fn process_create_vote_account( + rpc_client: &RpcClient, + config: &WalletConfig, + vote_account_pubkey: &Pubkey, + node_pubkey: &Pubkey, + commission: u8, + lamports: u64, +) -> ProcessResult { + check_unique_pubkeys( + (vote_account_pubkey, "vote_account_pubkey".to_string()), + (node_pubkey, "node_pubkey".to_string()), + )?; + check_unique_pubkeys( + (&config.keypair.pubkey(), "wallet keypair".to_string()), + (vote_account_pubkey, "vote_account_pubkey".to_string()), + )?; + let ixs = vote_instruction::create_account( + &config.keypair.pubkey(), + vote_account_pubkey, + node_pubkey, + commission, + lamports, + ); + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash); + check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; + let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); + log_instruction_custom_error::(result) +} + +pub fn process_authorize_voter( + rpc_client: &RpcClient, + config: &WalletConfig, + vote_account_pubkey: &Pubkey, + authorized_voter_keypair: &Keypair, + new_authorized_voter_pubkey: &Pubkey, +) -> ProcessResult { + check_unique_pubkeys( + (vote_account_pubkey, "vote_account_pubkey".to_string()), + ( + new_authorized_voter_pubkey, + "new_authorized_voter_pubkey".to_string(), + ), + )?; + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; + let ixs = vec![vote_instruction::authorize_voter( + vote_account_pubkey, // vote account to update + &authorized_voter_keypair.pubkey(), // current authorized voter (often the vote account itself) + new_authorized_voter_pubkey, // new vote signer + )]; + + let mut tx = Transaction::new_signed_with_payer( + ixs, + Some(&config.keypair.pubkey()), + &[&config.keypair, &authorized_voter_keypair], + recent_blockhash, + ); + check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; + let result = rpc_client + .send_and_confirm_transaction(&mut tx, &[&config.keypair, &authorized_voter_keypair]); + log_instruction_custom_error::(result) +} + +pub fn parse_vote_uptime_command(matches: &ArgMatches<'_>) -> Result { + let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); + let aggregate = matches.is_present("aggregate"); + let span = if matches.is_present("span") { + Some(value_t_or_exit!(matches, "span", u64)) + } else { + None + }; + Ok(WalletCommand::Uptime { + pubkey: vote_account_pubkey, + aggregate, + span, + }) +} + +pub fn process_show_vote_account( + rpc_client: &RpcClient, + _config: &WalletConfig, + vote_account_pubkey: &Pubkey, +) -> ProcessResult { + let vote_account = rpc_client.get_account(vote_account_pubkey)?; + + if vote_account.owner != solana_vote_api::id() { + Err(WalletError::RpcRequestError( + format!("{:?} is not a vote account", vote_account_pubkey).to_string(), + ))?; + } + + let vote_state = VoteState::deserialize(&vote_account.data).map_err(|_| { + WalletError::RpcRequestError( + "Account data could not be deserialized to vote state".to_string(), + ) + })?; + + println!("account lamports: {}", vote_account.lamports); + println!("node id: {}", vote_state.node_pubkey); + println!( + "authorized voter pubkey: {}", + vote_state.authorized_voter_pubkey + ); + println!("credits: {}", vote_state.credits()); + println!( + "commission: {}%", + f64::from(vote_state.commission) / f64::from(std::u32::MAX) + ); + println!( + "root slot: {}", + match vote_state.root_slot { + Some(slot) => slot.to_string(), + None => "~".to_string(), + } + ); + 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 + ); + } + + // TODO: Use the real GenesisBlock from the cluster. + let genesis_block = solana_sdk::genesis_block::GenesisBlock::default(); + let epoch_schedule = solana_runtime::epoch_schedule::EpochSchedule::new( + genesis_block.slots_per_epoch, + genesis_block.stakers_slot_offset, + genesis_block.epoch_warmup, + ); + + 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, + ); + } + } + Ok("".to_string()) +} + +pub fn process_uptime( + rpc_client: &RpcClient, + _config: &WalletConfig, + vote_account_pubkey: &Pubkey, + aggregate: bool, + span: Option, +) -> ProcessResult { + let vote_account = rpc_client.get_account(vote_account_pubkey)?; + + if vote_account.owner != solana_vote_api::id() { + Err(WalletError::RpcRequestError( + format!("{:?} is not a vote account", vote_account_pubkey).to_string(), + ))?; + } + + let vote_state = VoteState::deserialize(&vote_account.data).map_err(|_| { + WalletError::RpcRequestError( + "Account data could not be deserialized to vote state".to_string(), + ) + })?; + + println!("Node id: {}", vote_state.node_pubkey); + println!( + "Authorized voter pubkey: {}", + vote_state.authorized_voter_pubkey + ); + if !vote_state.votes.is_empty() { + println!("Uptime:"); + + // TODO: Use the real GenesisBlock from the cluster. + let genesis_block = solana_sdk::genesis_block::GenesisBlock::default(); + let epoch_schedule = solana_runtime::epoch_schedule::EpochSchedule::new( + genesis_block.slots_per_epoch, + genesis_block.stakers_slot_offset, + genesis_block.epoch_warmup, + ); + + let epoch_credits_vec: Vec<(u64, u64, u64)> = vote_state.epoch_credits().copied().collect(); + + let epoch_credits = if let Some(x) = span { + epoch_credits_vec.iter().rev().take(x as usize) + } else { + epoch_credits_vec.iter().rev().take(epoch_credits_vec.len()) + }; + + if aggregate { + let (credits_earned, slots_in_epoch, epochs): (u64, u64, u64) = + epoch_credits.fold((0, 0, 0), |acc, (epoch, credits, prev_credits)| { + let credits_earned = credits - prev_credits; + let slots_in_epoch = epoch_schedule.get_slots_in_epoch(*epoch); + (acc.0 + credits_earned, acc.1 + slots_in_epoch, acc.2 + 1) + }); + let total_uptime = credits_earned as f64 / slots_in_epoch as f64; + println!("{:.2}% over {} epochs", total_uptime * 100_f64, epochs,); + } else { + for (epoch, credits, prev_credits) in epoch_credits { + let credits_earned = credits - prev_credits; + let slots_in_epoch = epoch_schedule.get_slots_in_epoch(*epoch); + let uptime = credits_earned as f64 / slots_in_epoch as f64; + println!("- epoch: {} {:.2}% uptime", epoch, uptime * 100_f64,); + } + } + if let Some(x) = span { + if x > epoch_credits_vec.len() as u64 { + println!("(span longer than available epochs)"); + } + } + } + Ok("".to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::wallet::{app, parse_command}; + use solana_sdk::signature::write_keypair; + use std::fs; + + #[test] + fn test_parse_command() { + let test_commands = app("test", "desc", "version"); + let pubkey = Pubkey::new_rand(); + let pubkey_string = format!("{}", pubkey); + + // Test AuthorizeVoter Subcommand + let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); + let keypair = Keypair::new(); + let keypair_file = format!("{}/tmp/keypair_file-{}", out_dir, keypair.pubkey()); + let _ = write_keypair(&keypair, &keypair_file).unwrap(); + + let test_authorize_voter = test_commands.clone().get_matches_from(vec![ + "test", + "authorize-voter", + &pubkey_string, + &keypair_file, + &pubkey_string, + ]); + assert_eq!( + parse_command(&pubkey, &test_authorize_voter).unwrap(), + WalletCommand::AuthorizeVoter(pubkey, keypair, pubkey) + ); + fs::remove_file(&keypair_file).unwrap(); + + // Test CreateVoteAccount SubCommand + let node_pubkey = Pubkey::new_rand(); + let node_pubkey_string = format!("{}", node_pubkey); + let test_create_vote_account = test_commands.clone().get_matches_from(vec![ + "test", + "create-vote-account", + &pubkey_string, + &node_pubkey_string, + "50", + "--commission", + "10", + ]); + assert_eq!( + parse_command(&pubkey, &test_create_vote_account).unwrap(), + WalletCommand::CreateVoteAccount(pubkey, node_pubkey, 10, 50) + ); + let test_create_vote_account2 = test_commands.clone().get_matches_from(vec![ + "test", + "create-vote-account", + &pubkey_string, + &node_pubkey_string, + "50", + ]); + assert_eq!( + parse_command(&pubkey, &test_create_vote_account2).unwrap(), + WalletCommand::CreateVoteAccount(pubkey, node_pubkey, 0, 50) + ); + + // Test Uptime Subcommand + let pubkey = Pubkey::new_rand(); + let matches = test_commands.clone().get_matches_from(vec![ + "test", + "uptime", + &pubkey.to_string(), + "--span", + "4", + "--aggregate", + ]); + assert_eq!( + parse_command(&pubkey, &matches).unwrap(), + WalletCommand::Uptime { + pubkey, + aggregate: true, + span: Some(4) + } + ); + } + // TODO: Add process tests +} diff --git a/cli/src/wallet.rs b/cli/src/wallet.rs index b2aa39a9ab..6a8e47fb39 100644 --- a/cli/src/wallet.rs +++ b/cli/src/wallet.rs @@ -1,6 +1,6 @@ use crate::{ - display::println_name_value, input_validators::*, lamports_to_sol, sol_to_lamports, - validator_info::*, + display::println_name_value, input_parsers::*, input_validators::*, lamports_to_sol, + sol_to_lamports, validator_info::*, vote::*, }; use chrono::prelude::*; use clap::{value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; @@ -26,17 +26,14 @@ use solana_sdk::{ loader_instruction, message::Message, pubkey::Pubkey, - signature::{read_keypair, Keypair, KeypairUtil, Signature}, + signature::{Keypair, KeypairUtil, Signature}, system_instruction::SystemError, system_transaction, transaction::{Transaction, TransactionError}, }; use solana_stake_api::stake_instruction::{self, StakeError}; use solana_storage_api::storage_instruction; -use solana_vote_api::{ - vote_instruction::{self, VoteError}, - vote_state::VoteState, -}; +use solana_vote_api::vote_state::VoteState; use std::collections::VecDeque; use std::fs::File; use std::io::{Read, Write}; @@ -75,6 +72,11 @@ pub enum WalletCommand { use_lamports_unit: bool, }, ShowVoteAccount(Pubkey), + Uptime { + pubkey: Pubkey, + aggregate: bool, + span: Option, + }, DelegateStake(Keypair, Pubkey, u64, bool), WithdrawStake(Keypair, Pubkey, u64), DeactivateStake(Keypair, Pubkey), @@ -160,45 +162,6 @@ impl Default for WalletConfig { } } -// Return parsed values from matches at `name` -fn values_of(matches: &ArgMatches<'_>, name: &str) -> Option> -where - T: std::str::FromStr, - ::Err: std::fmt::Debug, -{ - matches - .values_of(name) - .map(|xs| xs.map(|x| x.parse::().unwrap()).collect()) -} - -// Return a parsed value from matches at `name` -fn value_of(matches: &ArgMatches<'_>, name: &str) -> Option -where - T: std::str::FromStr, - ::Err: std::fmt::Debug, -{ - if let Some(value) = matches.value_of(name) { - value.parse::().ok() - } else { - None - } -} - -// Return the keypair for an argument with filename `name` or None if not present. -fn keypair_of(matches: &ArgMatches<'_>, name: &str) -> Option { - if let Some(value) = matches.value_of(name) { - read_keypair(value).ok() - } else { - None - } -} - -// Return a pubkey for an argument that can itself be parsed into a pubkey, -// or is a filename that can be read as a keypair -fn pubkey_of(matches: &ArgMatches<'_>, name: &str) -> Option { - value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey())) -} - pub fn parse_command( pubkey: &Pubkey, matches: &ArgMatches<'_>, @@ -262,35 +225,6 @@ pub fn parse_command( } } } - ("create-vote-account", Some(matches)) => { - let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); - let node_pubkey = pubkey_of(matches, "node_pubkey").unwrap(); - let commission = if let Some(commission) = matches.value_of("commission") { - commission.parse()? - } else { - 0 - }; - let lamports = matches.value_of("lamports").unwrap().parse()?; - Ok(WalletCommand::CreateVoteAccount( - vote_account_pubkey, - node_pubkey, - commission, - lamports, - )) - } - ("authorize-voter", Some(matches)) => { - let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); - let authorized_voter_keypair = - keypair_of(matches, "authorized_voter_keypair_file").unwrap(); - let new_authorized_voter_pubkey = - pubkey_of(matches, "new_authorized_voter_pubkey").unwrap(); - - Ok(WalletCommand::AuthorizeVoter( - vote_account_pubkey, - authorized_voter_keypair, - new_authorized_voter_pubkey, - )) - } ("show-account", Some(matches)) => { let account_pubkey = pubkey_of(matches, "account_pubkey").unwrap(); let output_file = matches.value_of("output_file"); @@ -301,10 +235,10 @@ pub fn parse_command( use_lamports_unit, }) } - ("show-vote-account", Some(matches)) => { - let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); - Ok(WalletCommand::ShowVoteAccount(vote_account_pubkey)) - } + ("create-vote-account", Some(matches)) => parse_vote_create_account(matches), + ("authorize-voter", Some(matches)) => parse_vote_authorize_voter(matches), + ("show-vote-account", Some(matches)) => parse_vote_get_account_command(matches), + ("uptime", Some(matches)) => parse_vote_uptime_command(matches), ("delegate-stake", Some(matches)) => { let stake_account_keypair = keypair_of(matches, "stake_account_keypair_file").unwrap(); let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap(); @@ -517,7 +451,7 @@ fn check_account_for_multiple_fees( Err(WalletError::InsufficientFundsForFee)? } -fn check_unique_pubkeys( +pub fn check_unique_pubkeys( pubkey0: (&Pubkey, String), pubkey1: (&Pubkey, String), ) -> Result<(), WalletError> { @@ -600,69 +534,6 @@ fn process_confirm(rpc_client: &RpcClient, signature: &Signature) -> ProcessResu } } -fn process_create_vote_account( - rpc_client: &RpcClient, - config: &WalletConfig, - vote_account_pubkey: &Pubkey, - node_pubkey: &Pubkey, - commission: u8, - lamports: u64, -) -> ProcessResult { - check_unique_pubkeys( - (vote_account_pubkey, "vote_account_pubkey".to_string()), - (node_pubkey, "node_pubkey".to_string()), - )?; - check_unique_pubkeys( - (&config.keypair.pubkey(), "wallet keypair".to_string()), - (vote_account_pubkey, "vote_account_pubkey".to_string()), - )?; - let ixs = vote_instruction::create_account( - &config.keypair.pubkey(), - vote_account_pubkey, - node_pubkey, - commission, - lamports, - ); - let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash); - check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; - let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]); - log_instruction_custom_error::(result) -} - -fn process_authorize_voter( - rpc_client: &RpcClient, - config: &WalletConfig, - vote_account_pubkey: &Pubkey, - authorized_voter_keypair: &Keypair, - new_authorized_voter_pubkey: &Pubkey, -) -> ProcessResult { - check_unique_pubkeys( - (vote_account_pubkey, "vote_account_pubkey".to_string()), - ( - new_authorized_voter_pubkey, - "new_authorized_voter_pubkey".to_string(), - ), - )?; - let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; - let ixs = vec![vote_instruction::authorize_voter( - vote_account_pubkey, // vote account to update - &authorized_voter_keypair.pubkey(), // current authorized voter (often the vote account itself) - new_authorized_voter_pubkey, // new vote signer - )]; - - let mut tx = Transaction::new_signed_with_payer( - ixs, - Some(&config.keypair.pubkey()), - &[&config.keypair, &authorized_voter_keypair], - recent_blockhash, - ); - check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?; - let result = rpc_client - .send_and_confirm_transaction(&mut tx, &[&config.keypair, &authorized_voter_keypair]); - log_instruction_custom_error::(result) -} - fn process_show_account( rpc_client: &RpcClient, _config: &WalletConfig, @@ -694,73 +565,6 @@ fn process_show_account( Ok("".to_string()) } -fn process_show_vote_account( - rpc_client: &RpcClient, - _config: &WalletConfig, - vote_account_pubkey: &Pubkey, -) -> ProcessResult { - let vote_account = rpc_client.get_account(vote_account_pubkey)?; - - if vote_account.owner != solana_vote_api::id() { - Err(WalletError::RpcRequestError( - format!("{:?} is not a vote account", vote_account_pubkey).to_string(), - ))?; - } - - let vote_state = VoteState::deserialize(&vote_account.data).map_err(|_| { - WalletError::RpcRequestError( - "Account data could not be deserialized to vote state".to_string(), - ) - })?; - - println!("account lamports: {}", vote_account.lamports); - println!("node id: {}", vote_state.node_pubkey); - println!( - "authorized voter pubkey: {}", - vote_state.authorized_voter_pubkey - ); - println!("credits: {}", vote_state.credits()); - println!( - "commission: {}%", - f64::from(vote_state.commission) / f64::from(std::u32::MAX) - ); - println!( - "root slot: {}", - match vote_state.root_slot { - Some(slot) => slot.to_string(), - None => "~".to_string(), - } - ); - 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 - ); - } - - // TODO: Use the real GenesisBlock from the cluster. - let genesis_block = solana_sdk::genesis_block::GenesisBlock::default(); - let epoch_schedule = solana_runtime::epoch_schedule::EpochSchedule::new( - genesis_block.slots_per_epoch, - genesis_block.stakers_slot_offset, - genesis_block.epoch_warmup, - ); - - 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, - ); - } - } - Ok("".to_string()) -} - fn process_deactivate_stake_account( rpc_client: &RpcClient, config: &WalletConfig, @@ -1507,6 +1311,12 @@ pub fn process_command(config: &WalletConfig) -> ProcessResult { process_show_vote_account(&rpc_client, config, &vote_account_pubkey) } + WalletCommand::Uptime { + pubkey: vote_account_pubkey, + aggregate, + span, + } => process_uptime(&rpc_client, config, &vote_account_pubkey, *aggregate, *span), + WalletCommand::DelegateStake( stake_account_keypair, vote_account_pubkey, @@ -1705,7 +1515,7 @@ pub fn request_and_confirm_airdrop( log_instruction_custom_error::(result) } -fn log_instruction_custom_error(result: Result) -> ProcessResult +pub fn log_instruction_custom_error(result: Result) -> ProcessResult where E: 'static + std::error::Error + DecodeError + FromPrimitive, { @@ -1943,6 +1753,31 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .help("Vote account pubkey"), ), ) + .subcommand( + SubCommand::with_name("uptime") + .about("Show the uptime of a validator, based on epoch voting history") + .arg( + Arg::with_name("vote_account_pubkey") + .index(1) + .value_name("VOTE ACCOUNT PUBKEY") + .takes_value(true) + .required(true) + .validator(is_pubkey_or_keypair) + .help("Vote account pubkey"), + ) + .arg( + Arg::with_name("span") + .long("span") + .value_name("NUM OF EPOCHS") + .takes_value(true) + .help("Number of recent epochs to examine") + ) + .arg( + Arg::with_name("aggregate") + .long("aggregate") + .help("Aggregate uptime data across span") + ), + ) .subcommand( SubCommand::with_name("delegate-stake") .about("Delegate stake to a vote account") @@ -2423,8 +2258,10 @@ mod tests { use super::*; use serde_json::Value; use solana_client::mock_rpc_client_request::SIGNATURE; - use solana_sdk::signature::gen_keypair_file; - use solana_sdk::transaction::TransactionError; + use solana_sdk::{ + signature::{gen_keypair_file, read_keypair}, + transaction::TransactionError, + }; use std::path::PathBuf; #[test] @@ -2513,51 +2350,6 @@ mod tests { .get_matches_from(vec!["test", "confirm", "deadbeef"]); assert!(parse_command(&pubkey, &test_bad_signature).is_err()); - // Test AuthorizeVoter Subcommand - let keypair_file = make_tmp_path("keypair_file"); - gen_keypair_file(&keypair_file).unwrap(); - let keypair = read_keypair(&keypair_file).unwrap(); - - let test_authorize_voter = test_commands.clone().get_matches_from(vec![ - "test", - "authorize-voter", - &pubkey_string, - &keypair_file, - &pubkey_string, - ]); - assert_eq!( - parse_command(&pubkey, &test_authorize_voter).unwrap(), - WalletCommand::AuthorizeVoter(pubkey, keypair, pubkey) - ); - - // Test CreateVoteAccount SubCommand - let node_pubkey = Pubkey::new_rand(); - let node_pubkey_string = format!("{}", node_pubkey); - let test_create_vote_account = test_commands.clone().get_matches_from(vec![ - "test", - "create-vote-account", - &pubkey_string, - &node_pubkey_string, - "50", - "--commission", - "10", - ]); - assert_eq!( - parse_command(&pubkey, &test_create_vote_account).unwrap(), - WalletCommand::CreateVoteAccount(pubkey, node_pubkey, 10, 50) - ); - let test_create_vote_account2 = test_commands.clone().get_matches_from(vec![ - "test", - "create-vote-account", - &pubkey_string, - &node_pubkey_string, - "50", - ]); - assert_eq!( - parse_command(&pubkey, &test_create_vote_account2).unwrap(), - WalletCommand::CreateVoteAccount(pubkey, node_pubkey, 0, 50) - ); - // Test DelegateStake Subcommand fn make_tmp_path(name: &str) -> String { let out_dir = std::env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());