2019-10-04 19:54:09 -07:00
|
|
|
use crate::{
|
2019-10-06 21:47:24 -07:00
|
|
|
cli::{
|
|
|
|
build_balance_message, check_account_for_fee, CliCommand, CliConfig, CliError,
|
|
|
|
ProcessResult,
|
|
|
|
},
|
2019-10-04 19:54:09 -07:00
|
|
|
display::println_name_value,
|
|
|
|
};
|
|
|
|
use clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand};
|
|
|
|
use console::{style, Emoji};
|
|
|
|
use serde_json::Value;
|
|
|
|
use solana_client::rpc_client::RpcClient;
|
|
|
|
use solana_sdk::{
|
|
|
|
clock,
|
|
|
|
hash::Hash,
|
|
|
|
signature::{Keypair, KeypairUtil},
|
|
|
|
system_transaction,
|
|
|
|
};
|
|
|
|
use std::{
|
|
|
|
collections::VecDeque,
|
|
|
|
time::{Duration, Instant},
|
|
|
|
};
|
|
|
|
|
|
|
|
static CHECK_MARK: Emoji = Emoji("✅ ", "");
|
|
|
|
static CROSS_MARK: Emoji = Emoji("❌ ", "");
|
|
|
|
|
|
|
|
pub trait ClusterQuerySubCommands {
|
|
|
|
fn cluster_query_subcommands(self) -> Self;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ClusterQuerySubCommands for App<'_, '_> {
|
|
|
|
fn cluster_query_subcommands(self) -> Self {
|
|
|
|
self.subcommand(
|
|
|
|
SubCommand::with_name("cluster-version")
|
|
|
|
.about("Get the version of the cluster entrypoint"),
|
|
|
|
)
|
|
|
|
.subcommand(SubCommand::with_name("fees").about("Display current cluster fees"))
|
|
|
|
.subcommand(
|
|
|
|
SubCommand::with_name("get-epoch-info")
|
|
|
|
.about("Get information about the current epoch"),
|
|
|
|
)
|
|
|
|
.subcommand(
|
|
|
|
SubCommand::with_name("get-genesis-blockhash").about("Get the genesis blockhash"),
|
|
|
|
)
|
|
|
|
.subcommand(SubCommand::with_name("get-slot").about("Get current slot"))
|
|
|
|
.subcommand(
|
|
|
|
SubCommand::with_name("get-transaction-count").about("Get current transaction count"),
|
|
|
|
)
|
|
|
|
.subcommand(
|
|
|
|
SubCommand::with_name("ping")
|
|
|
|
.about("Submit transactions sequentially")
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("interval")
|
|
|
|
.short("i")
|
|
|
|
.long("interval")
|
|
|
|
.value_name("SECONDS")
|
|
|
|
.takes_value(true)
|
|
|
|
.default_value("2")
|
|
|
|
.help("Wait interval seconds between submitting the next transaction"),
|
|
|
|
)
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("count")
|
|
|
|
.short("c")
|
|
|
|
.long("count")
|
|
|
|
.value_name("NUMBER")
|
|
|
|
.takes_value(true)
|
|
|
|
.help("Stop after submitting count transactions"),
|
|
|
|
)
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("timeout")
|
|
|
|
.short("t")
|
|
|
|
.long("timeout")
|
|
|
|
.value_name("SECONDS")
|
|
|
|
.takes_value(true)
|
|
|
|
.default_value("10")
|
|
|
|
.help("Wait up to timeout seconds for transaction confirmation"),
|
|
|
|
),
|
|
|
|
)
|
2019-10-06 21:47:24 -07:00
|
|
|
.subcommand(
|
|
|
|
SubCommand::with_name("show-validators")
|
|
|
|
.about("Show information about the current validators")
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("lamports")
|
|
|
|
.long("lamports")
|
|
|
|
.takes_value(false)
|
|
|
|
.help("Display balance in lamports instead of SOL"),
|
|
|
|
),
|
|
|
|
)
|
2019-10-04 19:54:09 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn parse_cluster_ping(matches: &ArgMatches<'_>) -> Result<CliCommand, CliError> {
|
|
|
|
let interval = Duration::from_secs(value_t_or_exit!(matches, "interval", u64));
|
|
|
|
let count = if matches.is_present("count") {
|
|
|
|
Some(value_t_or_exit!(matches, "count", u64))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
let timeout = Duration::from_secs(value_t_or_exit!(matches, "timeout", u64));
|
|
|
|
Ok(CliCommand::Ping {
|
|
|
|
interval,
|
|
|
|
count,
|
|
|
|
timeout,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-10-06 21:47:24 -07:00
|
|
|
pub fn parse_show_validators(matches: &ArgMatches<'_>) -> Result<CliCommand, CliError> {
|
|
|
|
let use_lamports_unit = matches.is_present("lamports");
|
|
|
|
|
|
|
|
Ok(CliCommand::ShowValidators { use_lamports_unit })
|
|
|
|
}
|
|
|
|
|
2019-10-04 19:54:09 -07:00
|
|
|
pub fn process_cluster_version(rpc_client: &RpcClient, config: &CliConfig) -> ProcessResult {
|
|
|
|
let remote_version: Value = serde_json::from_str(&rpc_client.get_version()?)?;
|
|
|
|
println!(
|
|
|
|
"{} {}",
|
|
|
|
style("Cluster versions from:").bold(),
|
|
|
|
config.json_rpc_url
|
|
|
|
);
|
|
|
|
if let Some(versions) = remote_version.as_object() {
|
|
|
|
for (key, value) in versions.iter() {
|
|
|
|
if let Some(value_string) = value.as_str() {
|
|
|
|
println_name_value(&format!("* {}:", key), &value_string);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok("".to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn process_fees(rpc_client: &RpcClient) -> ProcessResult {
|
|
|
|
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
|
|
|
|
|
|
|
|
Ok(format!(
|
|
|
|
"blockhash: {}\nlamports per signature: {}",
|
|
|
|
recent_blockhash, fee_calculator.lamports_per_signature
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn process_get_epoch_info(rpc_client: &RpcClient) -> ProcessResult {
|
|
|
|
let epoch_info = rpc_client.get_epoch_info()?;
|
|
|
|
println!();
|
|
|
|
println_name_value("Current epoch:", &epoch_info.epoch.to_string());
|
|
|
|
println_name_value("Current slot:", &epoch_info.absolute_slot.to_string());
|
|
|
|
println_name_value(
|
|
|
|
"Total slots in current epoch:",
|
|
|
|
&epoch_info.slots_in_epoch.to_string(),
|
|
|
|
);
|
|
|
|
let remaining_slots_in_epoch = epoch_info.slots_in_epoch - epoch_info.slot_index;
|
|
|
|
println_name_value(
|
|
|
|
"Remaining slots in current epoch:",
|
|
|
|
&remaining_slots_in_epoch.to_string(),
|
|
|
|
);
|
|
|
|
|
|
|
|
let remaining_time_in_epoch = Duration::from_secs(
|
|
|
|
remaining_slots_in_epoch * clock::DEFAULT_TICKS_PER_SLOT / clock::DEFAULT_TICKS_PER_SECOND,
|
|
|
|
);
|
|
|
|
println_name_value(
|
|
|
|
"Time remaining in current epoch:",
|
|
|
|
&format!(
|
|
|
|
"{} minutes, {} seconds",
|
|
|
|
remaining_time_in_epoch.as_secs() / 60,
|
|
|
|
remaining_time_in_epoch.as_secs() % 60
|
|
|
|
),
|
|
|
|
);
|
|
|
|
Ok("".to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn process_get_genesis_blockhash(rpc_client: &RpcClient) -> ProcessResult {
|
|
|
|
let genesis_blockhash = rpc_client.get_genesis_blockhash()?;
|
|
|
|
Ok(genesis_blockhash.to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn process_get_slot(rpc_client: &RpcClient) -> ProcessResult {
|
|
|
|
let slot = rpc_client.get_slot()?;
|
|
|
|
Ok(slot.to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn process_get_transaction_count(rpc_client: &RpcClient) -> ProcessResult {
|
|
|
|
let transaction_count = rpc_client.get_transaction_count()?;
|
|
|
|
Ok(transaction_count.to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn process_ping(
|
|
|
|
rpc_client: &RpcClient,
|
|
|
|
config: &CliConfig,
|
|
|
|
interval: &Duration,
|
|
|
|
count: &Option<u64>,
|
|
|
|
timeout: &Duration,
|
|
|
|
) -> ProcessResult {
|
|
|
|
let to = Keypair::new().pubkey();
|
|
|
|
|
|
|
|
println_name_value("Source account:", &config.keypair.pubkey().to_string());
|
|
|
|
println_name_value("Destination account:", &to.to_string());
|
|
|
|
println!();
|
|
|
|
|
|
|
|
let (signal_sender, signal_receiver) = std::sync::mpsc::channel();
|
|
|
|
ctrlc::set_handler(move || {
|
|
|
|
let _ = signal_sender.send(());
|
|
|
|
})
|
|
|
|
.expect("Error setting Ctrl-C handler");
|
|
|
|
|
|
|
|
let mut last_blockhash = Hash::default();
|
|
|
|
let mut submit_count = 0;
|
|
|
|
let mut confirmed_count = 0;
|
|
|
|
let mut confirmation_time: VecDeque<u64> = VecDeque::with_capacity(1024);
|
|
|
|
|
|
|
|
'mainloop: for seq in 0..count.unwrap_or(std::u64::MAX) {
|
|
|
|
let (recent_blockhash, fee_calculator) = rpc_client.get_new_blockhash(&last_blockhash)?;
|
|
|
|
last_blockhash = recent_blockhash;
|
|
|
|
|
|
|
|
let transaction = system_transaction::transfer(&config.keypair, &to, 1, recent_blockhash);
|
|
|
|
check_account_for_fee(rpc_client, config, &fee_calculator, &transaction.message)?;
|
|
|
|
|
|
|
|
match rpc_client.send_transaction(&transaction) {
|
|
|
|
Ok(signature) => {
|
|
|
|
let transaction_sent = Instant::now();
|
|
|
|
loop {
|
|
|
|
let signature_status = rpc_client.get_signature_status(&signature)?;
|
|
|
|
let elapsed_time = Instant::now().duration_since(transaction_sent);
|
|
|
|
if let Some(transaction_status) = signature_status {
|
|
|
|
match transaction_status {
|
|
|
|
Ok(()) => {
|
|
|
|
let elapsed_time_millis = elapsed_time.as_millis() as u64;
|
|
|
|
confirmation_time.push_back(elapsed_time_millis);
|
|
|
|
println!(
|
|
|
|
"{}1 lamport transferred: seq={:<3} time={:>4}ms signature={}",
|
|
|
|
CHECK_MARK, seq, elapsed_time_millis, signature
|
|
|
|
);
|
|
|
|
confirmed_count += 1;
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
println!(
|
|
|
|
"{}Transaction failed: seq={:<3} error={:?} signature={}",
|
|
|
|
CROSS_MARK, seq, err, signature
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if elapsed_time >= *timeout {
|
|
|
|
println!(
|
|
|
|
"{}Confirmation timeout: seq={:<3} signature={}",
|
|
|
|
CROSS_MARK, seq, signature
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sleep for half a slot
|
|
|
|
if signal_receiver
|
|
|
|
.recv_timeout(Duration::from_millis(
|
|
|
|
500 * solana_sdk::clock::DEFAULT_TICKS_PER_SLOT
|
|
|
|
/ solana_sdk::clock::DEFAULT_TICKS_PER_SECOND,
|
|
|
|
))
|
|
|
|
.is_ok()
|
|
|
|
{
|
|
|
|
break 'mainloop;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
println!(
|
|
|
|
"{}Submit failed: seq={:<3} error={:?}",
|
|
|
|
CROSS_MARK, seq, err
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
submit_count += 1;
|
|
|
|
|
|
|
|
if signal_receiver.recv_timeout(*interval).is_ok() {
|
|
|
|
break 'mainloop;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
println!();
|
|
|
|
println!("--- transaction statistics ---");
|
|
|
|
println!(
|
|
|
|
"{} transactions submitted, {} transactions confirmed, {:.1}% transaction loss",
|
|
|
|
submit_count,
|
|
|
|
confirmed_count,
|
|
|
|
(100. - f64::from(confirmed_count) / f64::from(submit_count) * 100.)
|
|
|
|
);
|
|
|
|
if !confirmation_time.is_empty() {
|
|
|
|
let samples: Vec<f64> = confirmation_time.iter().map(|t| *t as f64).collect();
|
|
|
|
let dist = criterion_stats::Distribution::from(samples.into_boxed_slice());
|
|
|
|
let mean = dist.mean();
|
|
|
|
println!(
|
|
|
|
"confirmation min/mean/max/stddev = {:.0}/{:.0}/{:.0}/{:.0} ms",
|
|
|
|
dist.min(),
|
|
|
|
mean,
|
|
|
|
dist.max(),
|
|
|
|
dist.std_dev(Some(mean))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok("".to_string())
|
|
|
|
}
|
|
|
|
|
2019-10-06 21:47:24 -07:00
|
|
|
pub fn process_show_validators(rpc_client: &RpcClient, use_lamports_unit: bool) -> ProcessResult {
|
|
|
|
let vote_accounts = rpc_client.get_vote_accounts()?;
|
|
|
|
let total_activate_stake = vote_accounts
|
|
|
|
.current
|
|
|
|
.iter()
|
|
|
|
.chain(vote_accounts.delinquent.iter())
|
|
|
|
.fold(0, |acc, vote_account| acc + vote_account.activated_stake)
|
|
|
|
as f64;
|
|
|
|
|
|
|
|
println!(
|
|
|
|
"{}",
|
|
|
|
style(format!(
|
|
|
|
"{:<44} {:<44} {:<11} {:>10} {:>11} {}",
|
|
|
|
"Identity Pubkey",
|
|
|
|
"Vote Pubkey",
|
|
|
|
"Commission",
|
|
|
|
"Last Vote",
|
|
|
|
"Root Block",
|
|
|
|
"Active Stake",
|
|
|
|
))
|
|
|
|
.bold()
|
|
|
|
);
|
|
|
|
|
|
|
|
for vote_account in vote_accounts
|
|
|
|
.current
|
|
|
|
.iter()
|
|
|
|
.chain(vote_accounts.delinquent.iter())
|
|
|
|
{
|
|
|
|
fn non_zero_or_dash(v: u64) -> String {
|
|
|
|
if v == 0 {
|
|
|
|
"-".into()
|
|
|
|
} else {
|
|
|
|
format!("{}", v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
println!(
|
|
|
|
"{:<44} {:<44} {:>3} ({:>4.1}%) {:>10} {:>11} {:>11}",
|
|
|
|
vote_account.node_pubkey,
|
|
|
|
vote_account.vote_pubkey,
|
|
|
|
vote_account.commission,
|
|
|
|
f64::from(vote_account.commission) * 100.0 / f64::from(std::u8::MAX),
|
|
|
|
non_zero_or_dash(vote_account.last_vote),
|
|
|
|
non_zero_or_dash(vote_account.root_slot),
|
|
|
|
if vote_account.activated_stake > 0 {
|
|
|
|
format!(
|
|
|
|
"{} ({:.2}%)",
|
|
|
|
build_balance_message(vote_account.activated_stake, use_lamports_unit),
|
|
|
|
100. * vote_account.activated_stake as f64 / total_activate_stake
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
"-".into()
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok("".to_string())
|
|
|
|
}
|
|
|
|
|
2019-10-04 19:54:09 -07:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use crate::cli::{app, parse_command};
|
|
|
|
use solana_sdk::pubkey::Pubkey;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_command() {
|
|
|
|
let test_commands = app("test", "desc", "version");
|
|
|
|
let pubkey = Pubkey::new_rand();
|
|
|
|
|
|
|
|
let test_cluster_version = test_commands
|
|
|
|
.clone()
|
|
|
|
.get_matches_from(vec!["test", "cluster-version"]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_cluster_version).unwrap(),
|
|
|
|
CliCommand::ClusterVersion
|
|
|
|
);
|
|
|
|
|
|
|
|
let test_fees = test_commands.clone().get_matches_from(vec!["test", "fees"]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_fees).unwrap(),
|
|
|
|
CliCommand::Fees
|
|
|
|
);
|
|
|
|
|
|
|
|
let test_get_epoch_info = test_commands
|
|
|
|
.clone()
|
|
|
|
.get_matches_from(vec!["test", "get-epoch-info"]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_get_epoch_info).unwrap(),
|
|
|
|
CliCommand::GetEpochInfo
|
|
|
|
);
|
|
|
|
|
|
|
|
let test_get_genesis_blockhash = test_commands
|
|
|
|
.clone()
|
|
|
|
.get_matches_from(vec!["test", "get-genesis-blockhash"]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_get_genesis_blockhash).unwrap(),
|
|
|
|
CliCommand::GetGenesisBlockhash
|
|
|
|
);
|
|
|
|
|
|
|
|
let test_get_slot = test_commands
|
|
|
|
.clone()
|
|
|
|
.get_matches_from(vec!["test", "get-slot"]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_get_slot).unwrap(),
|
|
|
|
CliCommand::GetSlot
|
|
|
|
);
|
|
|
|
|
|
|
|
let test_transaction_count = test_commands
|
|
|
|
.clone()
|
|
|
|
.get_matches_from(vec!["test", "get-transaction-count"]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_transaction_count).unwrap(),
|
|
|
|
CliCommand::GetTransactionCount
|
|
|
|
);
|
|
|
|
|
|
|
|
let test_ping = test_commands
|
|
|
|
.clone()
|
|
|
|
.get_matches_from(vec!["test", "ping", "-i", "1", "-c", "2", "-t", "3"]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(&pubkey, &test_ping).unwrap(),
|
|
|
|
CliCommand::Ping {
|
|
|
|
interval: Duration::from_secs(1),
|
|
|
|
count: Some(2),
|
|
|
|
timeout: Duration::from_secs(3),
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// TODO: Add process tests
|
|
|
|
}
|