use crate::client::{ApiError, Network, Transport, info, info_request, user_request};
use crate::utils::{Address, Dec, HYPE_DECIMALS};
use serde::{Deserialize, Deserializer};
use serde_json::Value;
pub const MAINNET_DELEGATION_LOCK_MS: u64 = 86_400_000;
pub const TESTNET_DELEGATION_LOCK_MS: u64 = 120_000;
pub const MAINNET_UNSTAKING_MS: u64 = 7 * 86_400_000;
pub const TESTNET_UNSTAKING_MS: u64 = 300_000;
pub const MAX_PENDING_WITHDRAWALS: u64 = 5;
#[must_use]
pub const fn delegation_lock_ms(network: Network) -> u64 {
match network {
Network::Mainnet => MAINNET_DELEGATION_LOCK_MS,
Network::Testnet => TESTNET_DELEGATION_LOCK_MS,
}
}
#[must_use]
pub const fn unstaking_ms(network: Network) -> u64 {
match network {
Network::Mainnet => MAINNET_UNSTAKING_MS,
Network::Testnet => TESTNET_UNSTAKING_MS,
}
}
#[must_use]
pub const fn locked_until(network: Network, now_ms: u64) -> u64 {
now_ms.saturating_add(delegation_lock_ms(network))
}
#[must_use]
pub fn validator_summaries_request() -> String {
info_request("validatorSummaries", &[])
}
#[must_use]
pub fn delegator_summary_request(user: Address) -> String {
user_request("delegatorSummary", user)
}
#[must_use]
pub fn delegations_request(user: Address) -> String {
user_request("delegations", user)
}
#[must_use]
pub fn delegator_rewards_request(user: Address) -> String {
user_request("delegatorRewards", user)
}
#[must_use]
pub fn delegator_history_request(user: Address) -> String {
user_request("delegatorHistory", user)
}
pub async fn validator_summaries(transport: &impl Transport) -> Result<Vec<ValidatorSummary>, ApiError> {
info(transport, validator_summaries_request()).await
}
pub async fn delegator_summary(transport: &impl Transport, user: Address) -> Result<DelegatorSummary, ApiError> {
info(transport, delegator_summary_request(user)).await
}
pub async fn delegations(transport: &impl Transport, user: Address) -> Result<Vec<Delegation>, ApiError> {
info(transport, delegations_request(user)).await
}
pub async fn delegator_rewards(transport: &impl Transport, user: Address) -> Result<Vec<Reward>, ApiError> {
info(transport, delegator_rewards_request(user)).await
}
pub async fn delegator_history(transport: &impl Transport, user: Address) -> Result<Vec<StakeEvent>, ApiError> {
info(transport, delegator_history_request(user)).await
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidatorSummary {
pub validator: Address,
pub signer: Address,
#[serde(default)]
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub n_recent_blocks: u64,
pub stake: u64,
#[serde(default)]
pub is_jailed: bool,
#[serde(default)]
pub unjailable_after: Option<u64>,
#[serde(default)]
pub is_active: bool,
pub commission: Dec,
#[serde(default)]
pub stats: Vec<(String, ValidatorStats)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidatorStats {
pub uptime_fraction: Dec,
pub predicted_apr: Dec,
#[serde(default)]
pub n_samples: u64,
}
impl ValidatorSummary {
#[must_use]
pub fn stake_hype(&self) -> Option<Dec> {
Dec::new(i128::from(self.stake), HYPE_DECIMALS)
}
#[must_use]
pub fn stats(&self, window: &str) -> Option<&ValidatorStats> {
self.stats.iter().find(|(name, _)| name == window).map(|(_, stats)| stats)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DelegatorSummary {
pub delegated: Dec,
pub undelegated: Dec,
pub total_pending_withdrawal: Dec,
pub n_pending_withdrawals: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Delegation {
pub validator: Address,
pub amount: Dec,
pub locked_until_timestamp: u64,
}
impl Delegation {
#[must_use]
pub const fn is_locked(&self, now_ms: u64) -> bool {
now_ms < self.locked_until_timestamp
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Reward {
pub time: u64,
#[serde(default)]
pub source: String,
pub total_amount: Dec,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct StakeEvent {
pub time: u64,
pub hash: String,
#[serde(deserialize_with = "delta")]
pub delta: StakeDelta,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StakeDelta {
#[serde(rename_all = "camelCase")]
Delegate { validator: Address, amount: Dec, is_undelegate: bool },
Withdrawal { amount: Dec, phase: String },
CDeposit { amount: Dec },
#[serde(skip_deserializing)]
Other { kind: String },
}
impl StakeEvent {
#[must_use]
pub fn unstaked_at(&self, network: Network) -> Option<u64> {
match &self.delta {
StakeDelta::Withdrawal { phase, .. } if phase == "initiated" => {
Some(self.time.saturating_add(unstaking_ms(network)))
}
_ => None,
}
}
}
fn delta<'de, D: Deserializer<'de>>(deserializer: D) -> Result<StakeDelta, D::Error> {
let value = Value::deserialize(deserializer)?;
let kind = value.as_object().and_then(|object| object.keys().next().cloned()).unwrap_or_default();
Ok(serde_json::from_value(value).unwrap_or(StakeDelta::Other { kind }))
}
#[cfg(test)]
#[path = "tests/stake.rs"]
mod tests;