Skip to main content

df_hyperliquid_core/earn/staking/
account.rs

1//! An account's whole stake from its four reads: the summary, each
2//! delegation, the withdrawals still in the queue (dated from the history),
3//! and a month's rewards summed exactly.
4
5use crate::client::{ApiError, Network, Transport};
6use crate::earn::staking::reads::{
7    Delegation, DelegatorSummary, Reward, StakeDelta, StakeEvent, delegations, delegator_history, delegator_rewards,
8    delegator_summary,
9};
10use crate::utils::{Address, Dec};
11
12/// The window [`Staking::rewards`] sums: thirty days.
13pub const REWARDS_WINDOW_MS: u64 = 30 * 86_400_000;
14
15/// A withdrawal from staking still on its way to spot.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct PendingWithdrawal {
18    pub amount: Dec,
19    /// When it was asked for, milliseconds.
20    pub initiated: u64,
21    /// When it reaches spot.
22    pub ready_at: u64,
23}
24
25/// Withdrawals asked for by `now_ms` whose queue has not ended, newest first
26/// as the history lists them.
27#[must_use]
28pub fn pending_withdrawals(network: Network, history: &[StakeEvent], now_ms: u64) -> Vec<PendingWithdrawal> {
29    history
30        .iter()
31        .filter(|event| event.time <= now_ms)
32        .filter_map(|event| {
33            let StakeDelta::Withdrawal { amount, .. } = &event.delta else { return None };
34            let ready_at = event.unstaked_at(network)?;
35            (ready_at > now_ms).then_some(PendingWithdrawal { amount: *amount, initiated: event.time, ready_at })
36        })
37        .collect()
38}
39
40/// Every reward credited at or after `since_ms`, summed exactly. `None` past
41/// what a [`Dec`] holds.
42#[must_use]
43pub fn rewards_since(rewards: &[Reward], since_ms: u64) -> Option<Dec> {
44    Dec::checked_sum(rewards.iter().filter(|reward| reward.time >= since_ms).map(|reward| reward.total_amount))
45}
46
47/// One account's stake.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct Staking {
50    pub summary: DelegatorSummary,
51    pub delegations: Vec<Delegation>,
52    /// The newest withdrawals still queued, as many as the summary counts.
53    pub pending: Vec<PendingWithdrawal>,
54    /// Rewards over the last [`REWARDS_WINDOW_MS`].
55    pub rewards: Dec,
56}
57
58impl Staking {
59    #[must_use]
60    pub fn new(
61        network: Network,
62        summary: DelegatorSummary,
63        delegations: Vec<Delegation>,
64        rewards: &[Reward],
65        history: &[StakeEvent],
66        now_ms: u64,
67    ) -> Staking {
68        let count = usize::try_from(summary.n_pending_withdrawals).unwrap_or(usize::MAX);
69        let mut pending = pending_withdrawals(network, history, now_ms);
70        pending.truncate(count);
71        let rewards = rewards_since(rewards, now_ms.saturating_sub(REWARDS_WINDOW_MS)).unwrap_or(Dec::ZERO);
72        Staking { summary, delegations, pending, rewards }
73    }
74
75    /// Everything staked: delegated, the staking balance, and what is on its
76    /// way out, which is the account's until it lands in spot.
77    #[must_use]
78    pub fn total(&self) -> Option<Dec> {
79        let summary = &self.summary;
80        summary.delegated.checked_add(summary.undelegated)?.checked_add(summary.total_pending_withdrawal)
81    }
82
83    /// The delegation to `validator`, if any.
84    #[must_use]
85    pub fn delegation(&self, validator: Address) -> Option<&Delegation> {
86        self.delegations.iter().find(|delegation| delegation.validator == validator)
87    }
88}
89
90/// The four reads one after another, made into a [`Staking`] at `now_ms`.
91pub async fn staking(
92    transport: &impl Transport,
93    network: Network,
94    user: Address,
95    now_ms: u64,
96) -> Result<Staking, ApiError> {
97    let summary = delegator_summary(transport, user).await?;
98    let delegations = delegations(transport, user).await?;
99    let rewards = delegator_rewards(transport, user).await?;
100    let history = delegator_history(transport, user).await?;
101    Ok(Staking::new(network, summary, delegations, &rewards, &history, now_ms))
102}
103
104#[cfg(test)]
105#[path = "tests/staking.rs"]
106mod tests;