Display vote/stake account epoch rewards
This commit is contained in:
@ -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)]
|
#[derive(Default, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CliStakeState {
|
pub struct CliStakeState {
|
||||||
@ -600,6 +642,8 @@ pub struct CliStakeState {
|
|||||||
pub activating_stake: Option<u64>,
|
pub activating_stake: Option<u64>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub deactivating_stake: Option<u64>,
|
pub deactivating_stake: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub epoch_rewards: Option<Vec<CliEpochReward>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QuietDisplay for CliStakeState {}
|
impl QuietDisplay for CliStakeState {}
|
||||||
@ -753,13 +797,14 @@ impl fmt::Display for CliStakeState {
|
|||||||
}
|
}
|
||||||
show_authorized(f, self.authorized.as_ref().unwrap())?;
|
show_authorized(f, self.authorized.as_ref().unwrap())?;
|
||||||
show_lockup(f, self.lockup.as_ref())?;
|
show_lockup(f, self.lockup.as_ref())?;
|
||||||
|
show_epoch_rewards(f, &self.epoch_rewards)?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, PartialEq)]
|
||||||
pub enum CliStakeType {
|
pub enum CliStakeType {
|
||||||
Stake,
|
Stake,
|
||||||
RewardsPool,
|
RewardsPool,
|
||||||
@ -936,6 +981,8 @@ pub struct CliVoteAccount {
|
|||||||
pub epoch_voting_history: Vec<CliEpochVotingHistory>,
|
pub epoch_voting_history: Vec<CliEpochVotingHistory>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub use_lamports_unit: bool,
|
pub use_lamports_unit: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub epoch_rewards: Option<Vec<CliEpochReward>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QuietDisplay for CliVoteAccount {}
|
impl QuietDisplay for CliVoteAccount {}
|
||||||
@ -980,6 +1027,7 @@ impl fmt::Display for CliVoteAccount {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
show_epoch_rewards(f, &self.epoch_rewards)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -713,6 +713,7 @@ pub fn process_get_block(rpc_client: &RpcClient, _config: &CliConfig, slot: Slot
|
|||||||
}
|
}
|
||||||
if !block.rewards.is_empty() {
|
if !block.rewards.is_empty() {
|
||||||
block.rewards.sort_by(|a, b| a.pubkey.cmp(&b.pubkey));
|
block.rewards.sort_by(|a, b| a.pubkey.cmp(&b.pubkey));
|
||||||
|
let mut total_rewards = 0;
|
||||||
println!("Rewards:",);
|
println!("Rewards:",);
|
||||||
println!(
|
println!(
|
||||||
" {:<44} {:<15} {:<13} {:>14}",
|
" {:<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 {
|
for reward in block.rewards {
|
||||||
let sign = if reward.lamports < 0 { "-" } else { "" };
|
let sign = if reward.lamports < 0 { "-" } else { "" };
|
||||||
|
|
||||||
|
total_rewards += reward.lamports;
|
||||||
println!(
|
println!(
|
||||||
" {:<44} {:>15} {}",
|
" {:<44} {:>15} {}",
|
||||||
reward.pubkey,
|
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() {
|
for (index, transaction_with_meta) in block.transactions.iter().enumerate() {
|
||||||
println!("Transaction {}:", index);
|
println!("Transaction {}:", index);
|
||||||
|
106
cli/src/stake.rs
106
cli/src/stake.rs
@ -7,6 +7,7 @@ use crate::{
|
|||||||
nonce::check_nonce_account,
|
nonce::check_nonce_account,
|
||||||
spend_utils::{resolve_spend_tx_and_check_account_balances, SpendAmount},
|
spend_utils::{resolve_spend_tx_and_check_account_balances, SpendAmount},
|
||||||
};
|
};
|
||||||
|
use chrono::{Local, TimeZone};
|
||||||
use clap::{App, Arg, ArgGroup, ArgMatches, SubCommand};
|
use clap::{App, Arg, ArgGroup, ArgMatches, SubCommand};
|
||||||
use solana_clap_utils::{
|
use solana_clap_utils::{
|
||||||
fee_payer::{fee_payer_arg, FEE_PAYER_ARG},
|
fee_payer::{fee_payer_arg, FEE_PAYER_ARG},
|
||||||
@ -18,7 +19,8 @@ use solana_clap_utils::{
|
|||||||
ArgConstant,
|
ArgConstant,
|
||||||
};
|
};
|
||||||
use solana_cli_output::{
|
use solana_cli_output::{
|
||||||
return_signers, CliStakeHistory, CliStakeHistoryEntry, CliStakeState, CliStakeType,
|
return_signers, CliEpochReward, CliStakeHistory, CliStakeHistoryEntry, CliStakeState,
|
||||||
|
CliStakeType,
|
||||||
};
|
};
|
||||||
use solana_client::{
|
use solana_client::{
|
||||||
blockhash_query::BlockhashQuery, nonce_utils, rpc_client::RpcClient,
|
blockhash_query::BlockhashQuery, nonce_utils, rpc_client::RpcClient,
|
||||||
@ -27,7 +29,7 @@ use solana_client::{
|
|||||||
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
|
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
|
||||||
use solana_sdk::{
|
use solana_sdk::{
|
||||||
account_utils::StateMut,
|
account_utils::StateMut,
|
||||||
clock::Clock,
|
clock::{Clock, Epoch, Slot, UnixTimestamp},
|
||||||
message::Message,
|
message::Message,
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
system_instruction::SystemError,
|
system_instruction::SystemError,
|
||||||
@ -43,7 +45,7 @@ use solana_stake_program::{
|
|||||||
stake_state::{Authorized, Lockup, Meta, StakeAuthorize, StakeState},
|
stake_state::{Authorized, Lockup, Meta, StakeAuthorize, StakeState},
|
||||||
};
|
};
|
||||||
use solana_vote_program::vote_state::VoteState;
|
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 {
|
pub const STAKE_AUTHORITY_ARG: ArgConstant<'static> = ArgConstant {
|
||||||
name: "stake_authority",
|
name: "stake_authority",
|
||||||
@ -1543,6 +1545,7 @@ pub fn build_stake_state(
|
|||||||
active_stake: u64_some_if_not_zero(active_stake),
|
active_stake: u64_some_if_not_zero(active_stake),
|
||||||
activating_stake: u64_some_if_not_zero(activating_stake),
|
activating_stake: u64_some_if_not_zero(activating_stake),
|
||||||
deactivating_stake: u64_some_if_not_zero(deactivating_stake),
|
deactivating_stake: u64_some_if_not_zero(deactivating_stake),
|
||||||
|
..CliStakeState::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StakeState::RewardsPool => CliStakeState {
|
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(
|
pub fn process_show_stake_account(
|
||||||
rpc_client: &RpcClient,
|
rpc_client: &RpcClient,
|
||||||
config: &CliConfig,
|
config: &CliConfig,
|
||||||
stake_account_pubkey: &Pubkey,
|
stake_account_address: &Pubkey,
|
||||||
use_lamports_unit: bool,
|
use_lamports_unit: bool,
|
||||||
) -> ProcessResult {
|
) -> 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() {
|
if stake_account.owner != solana_stake_program::id() {
|
||||||
return Err(CliError::RpcRequestError(format!(
|
return Err(CliError::RpcRequestError(format!(
|
||||||
"{:?} is not a stake account",
|
"{:?} is not a stake account",
|
||||||
stake_account_pubkey,
|
stake_account_address,
|
||||||
))
|
))
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
@ -1603,13 +1685,23 @@ pub fn process_show_stake_account(
|
|||||||
CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string())
|
CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let state = build_stake_state(
|
let mut state = build_stake_state(
|
||||||
stake_account.lamports,
|
stake_account.lamports,
|
||||||
&stake_state,
|
&stake_state,
|
||||||
use_lamports_unit,
|
use_lamports_unit,
|
||||||
&stake_history,
|
&stake_history,
|
||||||
&clock,
|
&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))
|
Ok(config.output_format.formatted_string(&state))
|
||||||
}
|
}
|
||||||
Err(err) => Err(CliError::RpcRequestError(format!(
|
Err(err) => Err(CliError::RpcRequestError(format!(
|
||||||
|
@ -671,11 +671,11 @@ fn get_vote_account(
|
|||||||
pub fn process_show_vote_account(
|
pub fn process_show_vote_account(
|
||||||
rpc_client: &RpcClient,
|
rpc_client: &RpcClient,
|
||||||
config: &CliConfig,
|
config: &CliConfig,
|
||||||
vote_account_pubkey: &Pubkey,
|
vote_account_address: &Pubkey,
|
||||||
use_lamports_unit: bool,
|
use_lamports_unit: bool,
|
||||||
) -> ProcessResult {
|
) -> ProcessResult {
|
||||||
let (vote_account, vote_state) =
|
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()?;
|
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 {
|
let vote_account_data = CliVoteAccount {
|
||||||
account_balance: vote_account.lamports,
|
account_balance: vote_account.lamports,
|
||||||
validator_identity: vote_state.node_pubkey.to_string(),
|
validator_identity: vote_state.node_pubkey.to_string(),
|
||||||
@ -708,6 +714,7 @@ pub fn process_show_vote_account(
|
|||||||
votes,
|
votes,
|
||||||
epoch_voting_history,
|
epoch_voting_history,
|
||||||
use_lamports_unit,
|
use_lamports_unit,
|
||||||
|
epoch_rewards,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(config.output_format.formatted_string(&vote_account_data))
|
Ok(config.output_format.formatted_string(&vote_account_data))
|
||||||
|
Reference in New Issue
Block a user