Add solana-cli uptime subcommand (#5944)

automerge
This commit is contained in:
Tyera Eulberg 2019-09-18 10:29:57 -06:00 committed by Grimes
parent 92295dea4f
commit c48c9be913
4 changed files with 562 additions and 258 deletions

157
cli/src/input_parsers.rs Normal file
View File

@ -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<T>(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<T>>
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
matches
.values_of(name)
.map(|xs| xs.map(|x| x.parse::<T>().unwrap()).collect())
}
// Return a parsed value from matches at `name`
pub fn value_of<T>(matches: &ArgMatches<'_>, name: &str) -> Option<T>
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
if let Some(value) = matches.value_of(name) {
value.parse::<T>().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<Keypair> {
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<Pubkey> {
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::<u64>(&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::<u64>(&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();
}
}

View File

@ -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 {

353
cli/src/vote.rs Normal file
View File

@ -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<WalletCommand, WalletError> {
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<WalletCommand, WalletError> {
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<WalletCommand, WalletError> {
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::<SystemError>(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::<VoteError>(result)
}
pub fn parse_vote_uptime_command(matches: &ArgMatches<'_>) -> Result<WalletCommand, WalletError> {
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<u64>,
) -> 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
}

View File

@ -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<u64>,
},
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<T>(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<T>>
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
matches
.values_of(name)
.map(|xs| xs.map(|x| x.parse::<T>().unwrap()).collect())
}
// Return a parsed value from matches at `name`
fn value_of<T>(matches: &ArgMatches<'_>, name: &str) -> Option<T>
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
if let Some(value) = matches.value_of(name) {
value.parse::<T>().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<Keypair> {
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<Pubkey> {
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::<SystemError>(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::<VoteError>(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::<SystemError>(result)
}
fn log_instruction_custom_error<E>(result: Result<String, ClientError>) -> ProcessResult
pub fn log_instruction_custom_error<E>(result: Result<String, ClientError>) -> ProcessResult
where
E: 'static + std::error::Error + DecodeError<E> + 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());