df_hyperliquid_core/earn/staking/
reads.rs1use 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
10pub const MAINNET_DELEGATION_LOCK_MS: u64 = 86_400_000;
12
13pub const TESTNET_DELEGATION_LOCK_MS: u64 = 120_000;
15
16pub const MAINNET_UNSTAKING_MS: u64 = 7 * 86_400_000;
19
20pub const TESTNET_UNSTAKING_MS: u64 = 300_000;
22
23pub const MAX_PENDING_WITHDRAWALS: u64 = 5;
25
26#[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#[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#[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#[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
88pub async fn delegator_rewards(transport: &impl Transport, user: Address) -> Result<Vec<Reward>, ApiError> {
90 info(transport, delegator_rewards_request(user)).await
91}
92
93pub 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 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 pub commission: Dec,
119 #[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 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 #[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 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 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 #[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#[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 Withdrawal { amount: Dec, phase: String },
200 CDeposit { amount: Dec },
202 #[serde(skip_deserializing)]
204 Other { kind: String },
205}
206
207impl StakeEvent {
208 #[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
220fn 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;