df_hyperliquid_core/earn/staking/
plan.rs1use crate::account::Signers;
6use crate::client::Network;
7use crate::earn::staking::actions::{CDeposit, CWithdraw, TokenDelegate};
8use crate::earn::staking::reads::{Delegation, DelegatorSummary, MAX_PENDING_WITHDRAWALS, locked_until, unstaking_ms};
9use crate::utils::{Address, BadUnits, Dec};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum Balance {
14 Undelegated,
17 Delegated(Address),
19 Pending,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Change {
26 Up(Dec),
27 Down(Dec),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct Effect {
38 pub balance: Balance,
39 pub change: Change,
40}
41
42impl Effect {
43 #[must_use]
45 pub fn landed(&self, before: Dec, after: Dec) -> bool {
46 match self.change {
47 Change::Up(amount) => before.checked_add(amount) == Some(after),
48 Change::Down(amount) => before.checked_sub(amount) == Some(after),
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Plan<A> {
56 pub network: Network,
57 pub action: A,
58 pub nonce: u64,
60 pub key: Signers,
61 pub effect: Effect,
62 pub free_at: Option<u64>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
69pub enum Refused {
70 #[error("{amount} HYPE is more than the {held} there is")]
71 MoreThan { amount: Dec, held: Dec },
72 #[error("nothing is delegated to {0}")]
73 NoDelegation(Address),
74 #[error("that delegation is locked until {until} ms")]
75 Locked { until: u64 },
76 #[error("{MAX_PENDING_WITHDRAWALS} withdrawals already wait in the queue, the most there may be")]
77 QueueFull,
78 #[error(transparent)]
79 Units(#[from] BadUnits),
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct Ctx {
85 pub network: Network,
86 pub nonce: u64,
87 pub now_ms: u64,
89}
90
91impl Ctx {
92 const fn plan<A>(&self, action: A, key: Signers, effect: Effect, free_at: Option<u64>) -> Plan<A> {
93 Plan { network: self.network, action, nonce: self.nonce, key, effect, free_at }
94 }
95
96 pub fn stake(&self, hype: Dec) -> Result<Plan<CDeposit>, Refused> {
98 let action = CDeposit::new(hype, self.nonce)?;
99 let effect = Effect { balance: Balance::Undelegated, change: Change::Up(hype) };
100 Ok(self.plan(action, CDeposit::KEY, effect, None))
101 }
102
103 pub fn unstake(&self, hype: Dec, summary: &DelegatorSummary) -> Result<Plan<CWithdraw>, Refused> {
106 let action = CWithdraw::new(hype, self.nonce)?;
107 at_most(hype, summary.undelegated)?;
108 if summary.n_pending_withdrawals >= MAX_PENDING_WITHDRAWALS {
109 return Err(Refused::QueueFull);
110 }
111 let effect = Effect { balance: Balance::Undelegated, change: Change::Down(hype) };
112 let ready = self.now_ms.saturating_add(unstaking_ms(self.network));
113 Ok(self.plan(action, CWithdraw::KEY, effect, Some(ready)))
114 }
115
116 pub fn delegate(
119 &self,
120 validator: Address,
121 hype: Dec,
122 summary: &DelegatorSummary,
123 ) -> Result<Plan<TokenDelegate>, Refused> {
124 let action = TokenDelegate::delegate(validator, hype, self.nonce)?;
125 at_most(hype, summary.undelegated)?;
126 let effect = Effect { balance: Balance::Undelegated, change: Change::Down(hype) };
127 Ok(self.plan(action, TokenDelegate::KEY, effect, Some(locked_until(self.network, self.now_ms))))
128 }
129
130 pub fn undelegate(
133 &self,
134 validator: Address,
135 hype: Dec,
136 delegations: &[Delegation],
137 ) -> Result<Plan<TokenDelegate>, Refused> {
138 let action = TokenDelegate::undelegate(validator, hype, self.nonce)?;
139 let held = delegations
140 .iter()
141 .find(|delegation| delegation.validator == validator)
142 .ok_or(Refused::NoDelegation(validator))?;
143 if held.is_locked(self.now_ms) {
144 return Err(Refused::Locked { until: held.locked_until_timestamp });
145 }
146 at_most(hype, held.amount)?;
147 let effect = Effect { balance: Balance::Undelegated, change: Change::Up(hype) };
148 Ok(self.plan(action, TokenDelegate::KEY, effect, None))
149 }
150}
151
152fn at_most(amount: Dec, held: Dec) -> Result<(), Refused> {
153 match amount > held {
154 true => Err(Refused::MoreThan { amount, held }),
155 false => Ok(()),
156 }
157}
158
159#[cfg(test)]
160#[path = "tests/plan.rs"]
161mod tests;