2019-09-18 10:29:57 -06:00
|
|
|
use crate::{
|
|
|
|
input_parsers::*,
|
|
|
|
wallet::{
|
2019-09-29 21:18:15 -07:00
|
|
|
build_balance_message, check_account_for_fee, check_unique_pubkeys,
|
|
|
|
log_instruction_custom_error, ProcessResult, WalletCommand, WalletConfig, WalletError,
|
2019-09-18 10:29:57 -06:00
|
|
|
},
|
|
|
|
};
|
|
|
|
use clap::{value_t_or_exit, ArgMatches};
|
|
|
|
use solana_client::rpc_client::RpcClient;
|
|
|
|
use solana_sdk::{
|
2019-09-29 21:18:15 -07:00
|
|
|
pubkey::Pubkey, signature::KeypairUtil, system_instruction::SystemError,
|
2019-09-18 10:29:57 -06:00
|
|
|
transaction::Transaction,
|
|
|
|
};
|
|
|
|
use solana_vote_api::{
|
|
|
|
vote_instruction::{self, VoteError},
|
2019-09-25 13:53:49 -07:00
|
|
|
vote_state::{VoteAuthorize, VoteInit, VoteState},
|
2019-09-18 10:29:57 -06:00
|
|
|
};
|
|
|
|
|
2019-09-29 21:18:15 -07:00
|
|
|
pub fn parse_vote_create_account(
|
|
|
|
pubkey: &Pubkey,
|
|
|
|
matches: &ArgMatches<'_>,
|
|
|
|
) -> Result<WalletCommand, WalletError> {
|
2019-09-18 10:29:57 -06:00
|
|
|
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);
|
2019-09-25 13:53:49 -07:00
|
|
|
let authorized_voter = pubkey_of(matches, "authorized_voter").unwrap_or(vote_account_pubkey);
|
2019-09-29 21:18:15 -07:00
|
|
|
let authorized_withdrawer = pubkey_of(matches, "authorized_withdrawer").unwrap_or(*pubkey);
|
2019-09-26 10:26:47 -07:00
|
|
|
|
2019-09-18 10:29:57 -06:00
|
|
|
Ok(WalletCommand::CreateVoteAccount(
|
|
|
|
vote_account_pubkey,
|
2019-09-25 13:53:49 -07:00
|
|
|
VoteInit {
|
|
|
|
node_pubkey,
|
|
|
|
authorized_voter,
|
|
|
|
authorized_withdrawer,
|
|
|
|
commission,
|
|
|
|
},
|
2019-09-18 10:29:57 -06:00
|
|
|
))
|
|
|
|
}
|
|
|
|
|
2019-09-25 13:53:49 -07:00
|
|
|
pub fn parse_vote_authorize(
|
|
|
|
matches: &ArgMatches<'_>,
|
|
|
|
vote_authorize: VoteAuthorize,
|
|
|
|
) -> Result<WalletCommand, WalletError> {
|
2019-09-18 10:29:57 -06:00
|
|
|
let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap();
|
2019-09-25 13:53:49 -07:00
|
|
|
let new_authorized_pubkey = pubkey_of(matches, "new_authorized_pubkey").unwrap();
|
2019-09-18 10:29:57 -06:00
|
|
|
|
2019-09-25 13:53:49 -07:00
|
|
|
Ok(WalletCommand::VoteAuthorize(
|
2019-09-18 10:29:57 -06:00
|
|
|
vote_account_pubkey,
|
2019-09-25 13:53:49 -07:00
|
|
|
new_authorized_pubkey,
|
|
|
|
vote_authorize,
|
2019-09-18 10:29:57 -06:00
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn parse_vote_get_account_command(
|
|
|
|
matches: &ArgMatches<'_>,
|
|
|
|
) -> Result<WalletCommand, WalletError> {
|
|
|
|
let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap();
|
2019-09-26 10:26:47 -07:00
|
|
|
let use_lamports_unit = matches.is_present("lamports");
|
|
|
|
Ok(WalletCommand::ShowVoteAccount {
|
|
|
|
pubkey: vote_account_pubkey,
|
|
|
|
use_lamports_unit,
|
|
|
|
})
|
2019-09-18 10:29:57 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn process_create_vote_account(
|
|
|
|
rpc_client: &RpcClient,
|
|
|
|
config: &WalletConfig,
|
|
|
|
vote_account_pubkey: &Pubkey,
|
2019-09-25 13:53:49 -07:00
|
|
|
vote_init: &VoteInit,
|
2019-09-18 10:29:57 -06:00
|
|
|
) -> ProcessResult {
|
|
|
|
check_unique_pubkeys(
|
|
|
|
(vote_account_pubkey, "vote_account_pubkey".to_string()),
|
2019-09-25 13:53:49 -07:00
|
|
|
(&vote_init.node_pubkey, "node_pubkey".to_string()),
|
2019-09-18 10:29:57 -06:00
|
|
|
)?;
|
|
|
|
check_unique_pubkeys(
|
|
|
|
(&config.keypair.pubkey(), "wallet keypair".to_string()),
|
|
|
|
(vote_account_pubkey, "vote_account_pubkey".to_string()),
|
|
|
|
)?;
|
2019-10-01 01:14:49 +05:30
|
|
|
let required_balance =
|
|
|
|
rpc_client.get_minimum_balance_for_rent_exemption(VoteState::size_of())?;
|
2019-09-18 10:29:57 -06:00
|
|
|
let ixs = vote_instruction::create_account(
|
|
|
|
&config.keypair.pubkey(),
|
|
|
|
vote_account_pubkey,
|
2019-09-25 13:53:49 -07:00
|
|
|
vote_init,
|
2019-10-01 01:14:49 +05:30
|
|
|
required_balance,
|
2019-09-18 10:29:57 -06:00
|
|
|
);
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2019-09-25 13:53:49 -07:00
|
|
|
pub fn process_vote_authorize(
|
2019-09-18 10:29:57 -06:00
|
|
|
rpc_client: &RpcClient,
|
|
|
|
config: &WalletConfig,
|
|
|
|
vote_account_pubkey: &Pubkey,
|
2019-09-25 13:53:49 -07:00
|
|
|
new_authorized_pubkey: &Pubkey,
|
|
|
|
vote_authorize: VoteAuthorize,
|
2019-09-18 10:29:57 -06:00
|
|
|
) -> ProcessResult {
|
|
|
|
check_unique_pubkeys(
|
|
|
|
(vote_account_pubkey, "vote_account_pubkey".to_string()),
|
2019-09-25 13:53:49 -07:00
|
|
|
(new_authorized_pubkey, "new_authorized_pubkey".to_string()),
|
2019-09-18 10:29:57 -06:00
|
|
|
)?;
|
|
|
|
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
|
2019-09-25 13:53:49 -07:00
|
|
|
let ixs = vec![vote_instruction::authorize(
|
2019-09-29 21:18:15 -07:00
|
|
|
vote_account_pubkey, // vote account to update
|
|
|
|
&config.keypair.pubkey(), // current authorized voter
|
|
|
|
new_authorized_pubkey, // new vote signer/withdrawer
|
|
|
|
vote_authorize, // vote or withdraw
|
2019-09-18 10:29:57 -06:00
|
|
|
)];
|
|
|
|
|
2019-09-29 21:18:15 -07:00
|
|
|
let mut tx = Transaction::new_signed_instructions(&[&config.keypair], ixs, recent_blockhash);
|
2019-09-18 10:29:57 -06:00
|
|
|
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
|
2019-09-29 21:18:15 -07:00
|
|
|
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]);
|
2019-09-18 10:29:57 -06:00
|
|
|
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,
|
2019-09-26 10:26:47 -07:00
|
|
|
use_lamports_unit: bool,
|
2019-09-18 10:29:57 -06:00
|
|
|
) -> 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(),
|
|
|
|
)
|
|
|
|
})?;
|
|
|
|
|
2019-09-26 10:26:47 -07:00
|
|
|
println!(
|
|
|
|
"account balance: {}",
|
2019-09-29 21:18:15 -07:00
|
|
|
build_balance_message(vote_account.lamports, use_lamports_unit)
|
2019-09-26 10:26:47 -07:00
|
|
|
);
|
2019-09-18 10:29:57 -06:00
|
|
|
println!("node id: {}", vote_state.node_pubkey);
|
2019-09-25 13:53:49 -07:00
|
|
|
println!("authorized voter: {}", vote_state.authorized_voter);
|
2019-09-18 10:29:57 -06:00
|
|
|
println!(
|
2019-09-25 13:53:49 -07:00
|
|
|
"authorized withdrawer: {}",
|
|
|
|
vote_state.authorized_withdrawer
|
2019-09-18 10:29:57 -06:00
|
|
|
);
|
|
|
|
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);
|
2019-09-25 13:53:49 -07:00
|
|
|
println!("Authorized voter: {}", vote_state.authorized_voter);
|
2019-09-18 10:29:57 -06:00
|
|
|
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};
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_command() {
|
|
|
|
let test_commands = app("test", "desc", "version");
|
|
|
|
let pubkey = Pubkey::new_rand();
|
2019-09-29 21:18:15 -07:00
|
|
|
let pubkey_string = pubkey.to_string();
|
2019-09-18 10:29:57 -06:00
|
|
|
|
|
|
|
let test_authorize_voter = test_commands.clone().get_matches_from(vec![
|
|
|
|
"test",
|
2019-09-25 13:53:49 -07:00
|
|
|
"vote-authorize-voter",
|
2019-09-18 10:29:57 -06:00
|
|
|
&pubkey_string,
|
|
|
|
&pubkey_string,
|
|
|
|
]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_authorize_voter).unwrap(),
|
2019-09-29 21:18:15 -07:00
|
|
|
WalletCommand::VoteAuthorize(pubkey, pubkey, VoteAuthorize::Voter)
|
2019-09-18 10:29:57 -06:00
|
|
|
);
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
"--commission",
|
|
|
|
"10",
|
|
|
|
]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_create_vote_account).unwrap(),
|
2019-09-25 13:53:49 -07:00
|
|
|
WalletCommand::CreateVoteAccount(
|
|
|
|
pubkey,
|
|
|
|
VoteInit {
|
|
|
|
node_pubkey,
|
|
|
|
authorized_voter: pubkey,
|
|
|
|
authorized_withdrawer: pubkey,
|
|
|
|
commission: 10
|
2019-10-01 01:14:49 +05:30
|
|
|
}
|
2019-09-25 13:53:49 -07:00
|
|
|
)
|
2019-09-18 10:29:57 -06:00
|
|
|
);
|
|
|
|
let test_create_vote_account2 = test_commands.clone().get_matches_from(vec![
|
|
|
|
"test",
|
|
|
|
"create-vote-account",
|
|
|
|
&pubkey_string,
|
|
|
|
&node_pubkey_string,
|
|
|
|
]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_create_vote_account2).unwrap(),
|
2019-09-25 13:53:49 -07:00
|
|
|
WalletCommand::CreateVoteAccount(
|
|
|
|
pubkey,
|
|
|
|
VoteInit {
|
|
|
|
node_pubkey,
|
|
|
|
authorized_voter: pubkey,
|
|
|
|
authorized_withdrawer: pubkey,
|
|
|
|
commission: 0
|
2019-10-01 01:14:49 +05:30
|
|
|
}
|
2019-09-25 13:53:49 -07:00
|
|
|
)
|
|
|
|
);
|
|
|
|
// test init with an authed voter
|
|
|
|
let authed = Pubkey::new_rand();
|
|
|
|
let test_create_vote_account3 = test_commands.clone().get_matches_from(vec![
|
|
|
|
"test",
|
|
|
|
"create-vote-account",
|
|
|
|
&pubkey_string,
|
|
|
|
&node_pubkey_string,
|
|
|
|
"--authorized-voter",
|
|
|
|
&authed.to_string(),
|
|
|
|
]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_create_vote_account3).unwrap(),
|
|
|
|
WalletCommand::CreateVoteAccount(
|
|
|
|
pubkey,
|
|
|
|
VoteInit {
|
|
|
|
node_pubkey,
|
|
|
|
authorized_voter: authed,
|
|
|
|
authorized_withdrawer: pubkey,
|
|
|
|
commission: 0
|
2019-10-01 01:14:49 +05:30
|
|
|
}
|
2019-09-25 13:53:49 -07:00
|
|
|
)
|
|
|
|
);
|
|
|
|
// test init with an authed withdrawer
|
|
|
|
let test_create_vote_account4 = test_commands.clone().get_matches_from(vec![
|
|
|
|
"test",
|
|
|
|
"create-vote-account",
|
|
|
|
&pubkey_string,
|
|
|
|
&node_pubkey_string,
|
|
|
|
"--authorized-withdrawer",
|
|
|
|
&authed.to_string(),
|
|
|
|
]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_create_vote_account4).unwrap(),
|
|
|
|
WalletCommand::CreateVoteAccount(
|
|
|
|
pubkey,
|
|
|
|
VoteInit {
|
|
|
|
node_pubkey,
|
|
|
|
authorized_voter: pubkey,
|
|
|
|
authorized_withdrawer: authed,
|
|
|
|
commission: 0
|
2019-10-01 01:14:49 +05:30
|
|
|
}
|
2019-09-25 13:53:49 -07:00
|
|
|
)
|
2019-09-18 10:29:57 -06:00
|
|
|
);
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|