From 4de0713aa35752540bfea22d8b1ed1e5730b8fd9 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Mon, 6 Jul 2020 13:28:40 -0600 Subject: [PATCH] Rpc: Add getStakeActivation endpoint (#10902) * Add getStakeActivation endpoint * Add docs * Update docs/src/apps/jsonrpc-api.md Co-authored-by: Michael Vines * Rework return type * Update docs * Rebase Co-authored-by: Michael Vines --- client/src/rpc_config.rs | 2 +- client/src/rpc_response.rs | 17 +++++++ core/src/rpc.rs | 87 ++++++++++++++++++++++++++++++++++++ docs/src/apps/jsonrpc-api.md | 36 +++++++++++++++ 4 files changed, 141 insertions(+), 1 deletion(-) diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index 091e9921e0..6c8677c59d 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -37,7 +37,7 @@ pub struct RpcLargestAccountsConfig { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RpcInflationConfig { +pub struct RpcStakeConfig { pub epoch: Option, #[serde(flatten)] pub commitment: Option, diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 9c6e9b5a45..327f333449 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -202,3 +202,20 @@ pub struct RpcSupply { pub non_circulating: u64, pub non_circulating_accounts: Vec, } + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub enum StakeActivationState { + Activating, + Active, + Deactivating, + Inactive, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RpcStakeActivation { + pub state: StakeActivationState, + pub active: u64, + pub inactive: u64, +} diff --git a/core/src/rpc.rs b/core/src/rpc.rs index f417e15772..7f044afe7c 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -30,6 +30,7 @@ use solana_runtime::{ log_collector::LogCollector, }; use solana_sdk::{ + account_utils::StateMut, clock::{Slot, UnixTimestamp}, commitment_config::{CommitmentConfig, CommitmentLevel}, epoch_info::EpochInfo, @@ -37,9 +38,12 @@ use solana_sdk::{ hash::Hash, pubkey::Pubkey, signature::Signature, + stake_history::StakeHistory, + sysvar::{stake_history, Sysvar}, timing::slot_duration_from_slots_per_year, transaction::{self, Transaction}, }; +use solana_stake_program::stake_state::StakeState; use solana_transaction_status::{ ConfirmedBlock, ConfirmedTransaction, TransactionStatus, UiTransactionEncoding, }; @@ -755,6 +759,67 @@ impl JsonRpcRequestProcessor { .get_first_available_block() .unwrap_or_default() } + + pub fn get_stake_activation( + &self, + pubkey: &Pubkey, + config: Option, + ) -> Result { + let config = config.unwrap_or_default(); + let bank = self.bank(config.commitment); + let epoch = config.epoch.unwrap_or_else(|| bank.epoch()); + if bank.epoch().saturating_sub(epoch) > solana_sdk::stake_history::MAX_ENTRIES as u64 { + return Err(Error::invalid_params(format!( + "Invalid param: epoch {:?} is too far in the past", + epoch + ))); + } + if epoch > bank.epoch() { + return Err(Error::invalid_params(format!( + "Invalid param: epoch {:?} has not yet started", + epoch + ))); + } + + let stake_account = bank + .get_account(pubkey) + .ok_or_else(|| Error::invalid_params("Invalid param: account not found".to_string()))?; + let stake_state: StakeState = stake_account + .state() + .map_err(|_| Error::invalid_params("Invalid param: not a stake account".to_string()))?; + let delegation = stake_state.delegation().ok_or_else(|| { + Error::invalid_params("Invalid param: stake account has not been delegated".to_string()) + })?; + + let stake_history_account = bank + .get_account(&stake_history::id()) + .ok_or_else(Error::internal_error)?; + let stake_history = + StakeHistory::from_account(&stake_history_account).ok_or_else(Error::internal_error)?; + + let (active, activating, deactivating) = + delegation.stake_activating_and_deactivating(epoch, Some(&stake_history)); + let stake_activation_state = if deactivating > 0 { + StakeActivationState::Deactivating + } else if activating > 0 { + StakeActivationState::Activating + } else if active > 0 { + StakeActivationState::Active + } else { + StakeActivationState::Inactive + }; + let inactive_stake = match stake_activation_state { + StakeActivationState::Activating => activating, + StakeActivationState::Active => 0, + StakeActivationState::Deactivating => delegation.stake.saturating_sub(active), + StakeActivationState::Inactive => delegation.stake, + }; + Ok(RpcStakeActivation { + state: stake_activation_state, + active, + inactive: inactive_stake, + }) + } } fn verify_filter(input: &RpcFilterType) -> Result<()> { @@ -1062,6 +1127,14 @@ pub trait RpcSol { #[rpc(meta, name = "getFirstAvailableBlock")] fn get_first_available_block(&self, meta: Self::Metadata) -> Result; + + #[rpc(meta, name = "getStakeActivation")] + fn get_stake_activation( + &self, + meta: Self::Metadata, + pubkey_str: String, + config: Option, + ) -> Result; } pub struct RpcSolImpl; @@ -1589,6 +1662,20 @@ impl RpcSol for RpcSolImpl { fn get_first_available_block(&self, meta: Self::Metadata) -> Result { Ok(meta.get_first_available_block()) } + + fn get_stake_activation( + &self, + meta: Self::Metadata, + pubkey_str: String, + config: Option, + ) -> Result { + debug!( + "get_stake_activation rpc request received: {:?}", + pubkey_str + ); + let pubkey = verify_pubkey(pubkey_str)?; + meta.get_stake_activation(&pubkey, config) + } } fn deserialize_bs58_transaction(bs58_transaction: String) -> Result<(Vec, Transaction)> { diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index c345531d0b..e5f77b7e0c 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -41,6 +41,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana- * [getSignatureStatuses](jsonrpc-api.md#getsignaturestatuses) * [getSlot](jsonrpc-api.md#getslot) * [getSlotLeader](jsonrpc-api.md#getslotleader) +* [getStakeActivation](jsonrpc-api.md#getstakeactivation) * [getSupply](jsonrpc-api.md#getsupply) * [getTransactionCount](jsonrpc-api.md#gettransactioncount) * [getVersion](jsonrpc-api.md#getversion) @@ -943,6 +944,41 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m {"jsonrpc":"2.0","result":"ENvAW7JScgYq6o4zKZwewtkzzJgDzuJAFxYasvmEQdpS","id":1} ``` +### getStakeActivation + +Returns epoch activation information for a stake account + +#### Parameters: + +* `` - Pubkey of stake account to query, as base-58 encoded string +* `` - (optional) Configuration object containing the following optional fields: + * (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + * (optional) `epoch: ` - epoch for which to calculate activation details. If parameter not provided, defaults to current epoch. + +#### Results: + +The result will be a JSON object with the following fields: + +* `state: ` - stake active during the epoch +* `inactive: ` - stake inactive during the epoch + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getStakeActivation", "params": ["CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT"]}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"active":197717120,"inactive":0,"state":"active"},"id":1} + +// Request with Epoch +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getStakeActivation", "params": ["CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT", {"epoch": 4}]}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"active":124429280,"inactive":73287840,"state":"activating"},"id":1} +``` + ### getSupply Returns information about the current supply.