Display vote/stake account epoch rewards

This commit is contained in:
Michael Vines 2020-10-07 14:38:17 -07:00
parent b3f695c83f
commit 346890ded3
4 changed files with 166 additions and 10 deletions

View File

@ -571,6 +571,48 @@ impl fmt::Display for CliKeyedStakeState {
}
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliEpochReward {
pub epoch: Epoch,
pub effective_slot: Slot,
pub amount: u64, // lamports
pub post_balance: u64, // lamports
pub percent_change: f64,
pub apr: f64,
}
fn show_epoch_rewards(
f: &mut fmt::Formatter,
epoch_rewards: &Option<Vec<CliEpochReward>>,
) -> fmt::Result {
if let Some(epoch_rewards) = epoch_rewards {
if epoch_rewards.is_empty() {
return Ok(());
}
writeln!(f, "Epoch Rewards:")?;
writeln!(
f,
" {:<8} {:<11} {:<15} {:<15} {:>14} {:>14}",
"Epoch", "Reward Slot", "Amount", "New Balance", "Percent Change", "APR"
)?;
for reward in epoch_rewards {
writeln!(
f,
" {:<8} {:<11} ◎{:<14.9} ◎{:<14.9} {:>13.9}% {:>13.9}%",
reward.epoch,
reward.effective_slot,
lamports_to_sol(reward.amount),
lamports_to_sol(reward.post_balance),
reward.percent_change,
reward.apr,
)?;
}
}
Ok(())
}
#[derive(Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliStakeState {
@ -600,6 +642,8 @@ pub struct CliStakeState {
pub activating_stake: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deactivating_stake: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub epoch_rewards: Option<Vec<CliEpochReward>>,
}
impl QuietDisplay for CliStakeState {}
@ -753,13 +797,14 @@ impl fmt::Display for CliStakeState {
}
show_authorized(f, self.authorized.as_ref().unwrap())?;
show_lockup(f, self.lockup.as_ref())?;
show_epoch_rewards(f, &self.epoch_rewards)?
}
}
Ok(())
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, PartialEq)]
pub enum CliStakeType {
Stake,
RewardsPool,
@ -936,6 +981,8 @@ pub struct CliVoteAccount {
pub epoch_voting_history: Vec<CliEpochVotingHistory>,
#[serde(skip_serializing)]
pub use_lamports_unit: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub epoch_rewards: Option<Vec<CliEpochReward>>,
}
impl QuietDisplay for CliVoteAccount {}
@ -980,6 +1027,7 @@ impl fmt::Display for CliVoteAccount {
)?;
}
}
show_epoch_rewards(f, &self.epoch_rewards)?;
Ok(())
}
}

View File

@ -713,6 +713,7 @@ pub fn process_get_block(rpc_client: &RpcClient, _config: &CliConfig, slot: Slot
}
if !block.rewards.is_empty() {
block.rewards.sort_by(|a, b| a.pubkey.cmp(&b.pubkey));
let mut total_rewards = 0;
println!("Rewards:",);
println!(
" {:<44} {:<15} {:<13} {:>14}",
@ -721,6 +722,7 @@ pub fn process_get_block(rpc_client: &RpcClient, _config: &CliConfig, slot: Slot
for reward in block.rewards {
let sign = if reward.lamports < 0 { "-" } else { "" };
total_rewards += reward.lamports;
println!(
" {:<44} {:>15} {}",
reward.pubkey,
@ -741,6 +743,13 @@ pub fn process_get_block(rpc_client: &RpcClient, _config: &CliConfig, slot: Slot
}
);
}
let sign = if total_rewards < 0 { "-" } else { "" };
println!(
"Total Rewards: {}◎{:12.9}",
sign,
lamports_to_sol(total_rewards.abs() as u64)
);
}
for (index, transaction_with_meta) in block.transactions.iter().enumerate() {
println!("Transaction {}:", index);

View File

@ -7,6 +7,7 @@ use crate::{
nonce::check_nonce_account,
spend_utils::{resolve_spend_tx_and_check_account_balances, SpendAmount},
};
use chrono::{Local, TimeZone};
use clap::{App, Arg, ArgGroup, ArgMatches, SubCommand};
use solana_clap_utils::{
fee_payer::{fee_payer_arg, FEE_PAYER_ARG},
@ -18,7 +19,8 @@ use solana_clap_utils::{
ArgConstant,
};
use solana_cli_output::{
return_signers, CliStakeHistory, CliStakeHistoryEntry, CliStakeState, CliStakeType,
return_signers, CliEpochReward, CliStakeHistory, CliStakeHistoryEntry, CliStakeState,
CliStakeType,
};
use solana_client::{
blockhash_query::BlockhashQuery, nonce_utils, rpc_client::RpcClient,
@ -27,7 +29,7 @@ use solana_client::{
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
use solana_sdk::{
account_utils::StateMut,
clock::Clock,
clock::{Clock, Epoch, Slot, UnixTimestamp},
message::Message,
pubkey::Pubkey,
system_instruction::SystemError,
@ -43,7 +45,7 @@ use solana_stake_program::{
stake_state::{Authorized, Lockup, Meta, StakeAuthorize, StakeState},
};
use solana_vote_program::vote_state::VoteState;
use std::{ops::Deref, sync::Arc};
use std::{convert::TryInto, ops::Deref, sync::Arc};
pub const STAKE_AUTHORITY_ARG: ArgConstant<'static> = ArgConstant {
name: "stake_authority",
@ -1543,6 +1545,7 @@ pub fn build_stake_state(
active_stake: u64_some_if_not_zero(active_stake),
activating_stake: u64_some_if_not_zero(activating_stake),
deactivating_stake: u64_some_if_not_zero(deactivating_stake),
..CliStakeState::default()
}
}
StakeState::RewardsPool => CliStakeState {
@ -1577,17 +1580,96 @@ pub fn build_stake_state(
}
}
pub(crate) fn fetch_epoch_rewards(
rpc_client: &RpcClient,
address: &Pubkey,
lowest_epoch: Epoch,
) -> Result<Vec<CliEpochReward>, Box<dyn std::error::Error>> {
let mut all_epoch_rewards = vec![];
let epoch_schedule = rpc_client.get_epoch_schedule()?;
let slot = rpc_client.get_slot()?;
let first_available_block = rpc_client.get_first_available_block()?;
let mut epoch = epoch_schedule.get_epoch_and_slot_index(slot).0;
let mut epoch_info: Option<(Slot, UnixTimestamp, solana_transaction_status::Rewards)> = None;
while epoch > lowest_epoch {
let first_slot_in_epoch = epoch_schedule.get_first_slot_in_epoch(epoch);
if first_slot_in_epoch < first_available_block {
// RPC node is out of history data
break;
}
let first_confirmed_block_in_epoch = *rpc_client
.get_confirmed_blocks_with_limit(first_slot_in_epoch, 1)?
.get(0)
.ok_or_else(|| format!("Unable to fetch first confirmed block for epoch {}", epoch))?;
let first_confirmed_block = rpc_client.get_confirmed_block_with_encoding(
first_confirmed_block_in_epoch,
solana_transaction_status::UiTransactionEncoding::Base64,
)?;
let epoch_start_time = if let Some(block_time) = first_confirmed_block.block_time {
block_time
} else {
break;
};
// Rewards for the previous epoch are found in the first confirmed block of the current epoch
let previous_epoch_rewards = first_confirmed_block.rewards;
if let Some((effective_slot, epoch_end_time, epoch_rewards)) = epoch_info {
let wall_clock_epoch_duration =
{ Local.timestamp(epoch_end_time, 0) - Local.timestamp(epoch_start_time, 0) }
.to_std()?
.as_secs_f64();
const SECONDS_PER_YEAR: f64 = (24 * 60 * 60 * 356) as f64;
let percent_of_year = SECONDS_PER_YEAR / wall_clock_epoch_duration;
if let Some(reward) = epoch_rewards
.into_iter()
.find(|reward| reward.pubkey == address.to_string())
{
if reward.post_balance > reward.lamports.try_into().unwrap_or(0) {
let balance_increase_percent = reward.lamports.abs() as f64
/ (reward.post_balance as f64 - reward.lamports as f64);
all_epoch_rewards.push(CliEpochReward {
epoch,
effective_slot,
amount: reward.lamports.abs() as u64,
post_balance: reward.post_balance,
percent_change: balance_increase_percent,
apr: balance_increase_percent * percent_of_year,
});
}
}
}
epoch -= 1;
epoch_info = Some((
first_confirmed_block_in_epoch,
epoch_start_time,
previous_epoch_rewards,
));
}
Ok(all_epoch_rewards)
}
pub fn process_show_stake_account(
rpc_client: &RpcClient,
config: &CliConfig,
stake_account_pubkey: &Pubkey,
stake_account_address: &Pubkey,
use_lamports_unit: bool,
) -> ProcessResult {
let stake_account = rpc_client.get_account(stake_account_pubkey)?;
let stake_account = rpc_client.get_account(stake_account_address)?;
if stake_account.owner != solana_stake_program::id() {
return Err(CliError::RpcRequestError(format!(
"{:?} is not a stake account",
stake_account_pubkey,
stake_account_address,
))
.into());
}
@ -1603,13 +1685,23 @@ pub fn process_show_stake_account(
CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string())
})?;
let state = build_stake_state(
let mut state = build_stake_state(
stake_account.lamports,
&stake_state,
use_lamports_unit,
&stake_history,
&clock,
);
if state.stake_type == CliStakeType::Stake {
if let Some(activation_epoch) = state.activation_epoch {
state.epoch_rewards = Some(fetch_epoch_rewards(
rpc_client,
stake_account_address,
activation_epoch,
)?);
}
}
Ok(config.output_format.formatted_string(&state))
}
Err(err) => Err(CliError::RpcRequestError(format!(

View File

@ -671,11 +671,11 @@ fn get_vote_account(
pub fn process_show_vote_account(
rpc_client: &RpcClient,
config: &CliConfig,
vote_account_pubkey: &Pubkey,
vote_account_address: &Pubkey,
use_lamports_unit: bool,
) -> ProcessResult {
let (vote_account, vote_state) =
get_vote_account(rpc_client, vote_account_pubkey, config.commitment)?;
get_vote_account(rpc_client, vote_account_address, config.commitment)?;
let epoch_schedule = rpc_client.get_epoch_schedule()?;
@ -696,6 +696,12 @@ pub fn process_show_vote_account(
}
}
let epoch_rewards = Some(crate::stake::fetch_epoch_rewards(
rpc_client,
vote_account_address,
1,
)?);
let vote_account_data = CliVoteAccount {
account_balance: vote_account.lamports,
validator_identity: vote_state.node_pubkey.to_string(),
@ -708,6 +714,7 @@ pub fn process_show_vote_account(
votes,
epoch_voting_history,
use_lamports_unit,
epoch_rewards,
};
Ok(config.output_format.formatted_string(&vote_account_data))