Skip to main content

df_hyperliquid_core/earn/staking/
reads.rs

1//! `validatorSummaries`, `delegatorSummary`, `delegations`,
2//! `delegatorRewards`, `delegatorHistory`. HYPE figures are decimal strings,
3//! except a validator's `stake`, which is wei.
4
5use crate::client::{ApiError, Network, Transport, info, info_request, user_request};
6use crate::utils::{Address, Dec, HYPE_DECIMALS};
7use serde::{Deserialize, Deserializer};
8use serde_json::Value;
9
10/// A mainnet delegation is locked one day before it can be undelegated.
11pub const MAINNET_DELEGATION_LOCK_MS: u64 = 86_400_000;
12
13/// Testnet's delegation lock: two minutes, as live testnet delegations show.
14pub const TESTNET_DELEGATION_LOCK_MS: u64 = 120_000;
15
16/// A mainnet withdrawal from staking waits seven days in the queue before it
17/// reaches spot.
18pub const MAINNET_UNSTAKING_MS: u64 = 7 * 86_400_000;
19
20/// Testnet's unstaking queue: five minutes, as live testnet histories show.
21pub const TESTNET_UNSTAKING_MS: u64 = 300_000;
22
23/// At most this many withdrawals wait in the queue at once.
24pub const MAX_PENDING_WITHDRAWALS: u64 = 5;
25
26/// How long a new delegation on `network` is locked.
27#[must_use]
28pub const fn delegation_lock_ms(network: Network) -> u64 {
29    match network {
30        Network::Mainnet => MAINNET_DELEGATION_LOCK_MS,
31        Network::Testnet => TESTNET_DELEGATION_LOCK_MS,
32    }
33}
34
35/// How long a withdrawal on `network` waits before it reaches spot.
36#[must_use]
37pub const fn unstaking_ms(network: Network) -> u64 {
38    match network {
39        Network::Mainnet => MAINNET_UNSTAKING_MS,
40        Network::Testnet => TESTNET_UNSTAKING_MS,
41    }
42}
43
44/// When a delegation made at `now_ms` may first be undelegated.
45#[must_use]
46pub const fn locked_until(network: Network, now_ms: u64) -> u64 {
47    now_ms.saturating_add(delegation_lock_ms(network))
48}
49
50/// `{"type":"validatorSummaries"}`.
51#[must_use]
52pub fn validator_summaries_request() -> String {
53    info_request("validatorSummaries", &[])
54}
55
56#[must_use]
57pub fn delegator_summary_request(user: Address) -> String {
58    user_request("delegatorSummary", user)
59}
60
61#[must_use]
62pub fn delegations_request(user: Address) -> String {
63    user_request("delegations", user)
64}
65
66#[must_use]
67pub fn delegator_rewards_request(user: Address) -> String {
68    user_request("delegatorRewards", user)
69}
70
71#[must_use]
72pub fn delegator_history_request(user: Address) -> String {
73    user_request("delegatorHistory", user)
74}
75
76pub async fn validator_summaries(transport: &impl Transport) -> Result<Vec<ValidatorSummary>, ApiError> {
77    info(transport, validator_summaries_request()).await
78}
79
80pub async fn delegator_summary(transport: &impl Transport, user: Address) -> Result<DelegatorSummary, ApiError> {
81    info(transport, delegator_summary_request(user)).await
82}
83
84pub async fn delegations(transport: &impl Transport, user: Address) -> Result<Vec<Delegation>, ApiError> {
85    info(transport, delegations_request(user)).await
86}
87
88/// Newest first.
89pub async fn delegator_rewards(transport: &impl Transport, user: Address) -> Result<Vec<Reward>, ApiError> {
90    info(transport, delegator_rewards_request(user)).await
91}
92
93/// Newest first.
94pub async fn delegator_history(transport: &impl Transport, user: Address) -> Result<Vec<StakeEvent>, ApiError> {
95    info(transport, delegator_history_request(user)).await
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct ValidatorSummary {
101    pub validator: Address,
102    pub signer: Address,
103    #[serde(default)]
104    pub name: String,
105    #[serde(default)]
106    pub description: String,
107    #[serde(default)]
108    pub n_recent_blocks: u64,
109    /// In wei, 1e8 to a HYPE.
110    pub stake: u64,
111    #[serde(default)]
112    pub is_jailed: bool,
113    #[serde(default)]
114    pub unjailable_after: Option<u64>,
115    #[serde(default)]
116    pub is_active: bool,
117    /// A fraction of rewards: 0.04 is 4%.
118    pub commission: Dec,
119    /// By window: `day`, `week`, `month`.
120    #[serde(default)]
121    pub stats: Vec<(String, ValidatorStats)>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct ValidatorStats {
127    pub uptime_fraction: Dec,
128    /// Yearly, as a fraction.
129    pub predicted_apr: Dec,
130    #[serde(default)]
131    pub n_samples: u64,
132}
133
134impl ValidatorSummary {
135    #[must_use]
136    pub fn stake_hype(&self) -> Option<Dec> {
137        Dec::new(i128::from(self.stake), HYPE_DECIMALS)
138    }
139
140    /// The stats over one window: `week` is what most clients show.
141    #[must_use]
142    pub fn stats(&self, window: &str) -> Option<&ValidatorStats> {
143        self.stats.iter().find(|(name, _)| name == window).map(|(_, stats)| stats)
144    }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct DelegatorSummary {
150    pub delegated: Dec,
151    /// The staking balance free to delegate or withdraw.
152    pub undelegated: Dec,
153    pub total_pending_withdrawal: Dec,
154    pub n_pending_withdrawals: u64,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct Delegation {
160    pub validator: Address,
161    pub amount: Dec,
162    /// Milliseconds; undelegating before it is refused.
163    pub locked_until_timestamp: u64,
164}
165
166impl Delegation {
167    #[must_use]
168    pub const fn is_locked(&self, now_ms: u64) -> bool {
169        now_ms < self.locked_until_timestamp
170    }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct Reward {
176    pub time: u64,
177    /// `delegation`, or `commission` for a validator's own cut.
178    #[serde(default)]
179    pub source: String,
180    pub total_amount: Dec,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
184pub struct StakeEvent {
185    pub time: u64,
186    pub hash: String,
187    #[serde(deserialize_with = "delta")]
188    pub delta: StakeDelta,
189}
190
191/// What a history row did, keyed by its one field's name.
192#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub enum StakeDelta {
195    #[serde(rename_all = "camelCase")]
196    Delegate { validator: Address, amount: Dec, is_undelegate: bool },
197    /// From staking toward spot: `initiated`, then `finalized` once queued
198    /// [`unstaking_ms`].
199    Withdrawal { amount: Dec, phase: String },
200    /// Spot into staking.
201    CDeposit { amount: Dec },
202    /// A kind this build does not read, by name.
203    #[serde(skip_deserializing)]
204    Other { kind: String },
205}
206
207impl StakeEvent {
208    /// When an initiated withdrawal reaches spot on `network`.
209    #[must_use]
210    pub fn unstaked_at(&self, network: Network) -> Option<u64> {
211        match &self.delta {
212            StakeDelta::Withdrawal { phase, .. } if phase == "initiated" => {
213                Some(self.time.saturating_add(unstaking_ms(network)))
214            }
215            _ => None,
216        }
217    }
218}
219
220/// A known kind that no longer reads as modelled falls back to `Other` too:
221/// one changed row must not blank the history.
222fn delta<'de, D: Deserializer<'de>>(deserializer: D) -> Result<StakeDelta, D::Error> {
223    let value = Value::deserialize(deserializer)?;
224    let kind = value.as_object().and_then(|object| object.keys().next().cloned()).unwrap_or_default();
225    Ok(serde_json::from_value(value).unwrap_or(StakeDelta::Other { kind }))
226}
227
228#[cfg(test)]
229#[path = "tests/stake.rs"]
230mod tests;