df_hyperliquid_core/earn/staking/
plan.rs1use crate::account::signers::ActionKind;
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
11pub use crate::account::signers::Signers as Key;
14
15const KINDS: [ActionKind; 3] = [ActionKind::CDeposit, ActionKind::CWithdraw, ActionKind::TokenDelegate];
17
18#[must_use]
20pub fn key(kind: &str) -> Option<Key> {
21 ActionKind::from_wire(kind).filter(|kind| KINDS.contains(kind)).map(ActionKind::signers)
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum Balance {
27 Undelegated,
30 Delegated(Address),
32 Pending,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Change {
39 Up(Dec),
40 Down(Dec),
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct Effect {
51 pub balance: Balance,
52 pub change: Change,
53}
54
55impl Effect {
56 #[must_use]
58 pub fn landed(&self, before: Dec, after: Dec) -> bool {
59 match self.change {
60 Change::Up(amount) => before.checked_add(amount) == Some(after),
61 Change::Down(amount) => before.checked_sub(amount) == Some(after),
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct Plan<A> {
69 pub network: Network,
70 pub action: A,
71 pub nonce: u64,
73 pub key: Key,
74 pub effect: Effect,
75 pub free_at: Option<u64>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
82pub enum Refused {
83 #[error("{amount} HYPE is more than the {held} there is")]
84 MoreThan { amount: Dec, held: Dec },
85 #[error("nothing is delegated to {0}")]
86 NoDelegation(Address),
87 #[error("that delegation is locked until {until} ms")]
88 Locked { until: u64 },
89 #[error("{MAX_PENDING_WITHDRAWALS} withdrawals already wait in the queue, the most there may be")]
90 QueueFull,
91 #[error(transparent)]
92 Units(#[from] BadUnits),
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct Ctx {
98 pub network: Network,
99 pub nonce: u64,
100 pub now_ms: u64,
102}
103
104impl Ctx {
105 const fn plan<A>(&self, action: A, key: Key, effect: Effect, free_at: Option<u64>) -> Plan<A> {
106 Plan { network: self.network, action, nonce: self.nonce, key, effect, free_at }
107 }
108
109 pub fn stake(&self, hype: Dec) -> Result<Plan<CDeposit>, Refused> {
111 let action = CDeposit::new(hype, self.nonce)?;
112 let effect = Effect { balance: Balance::Undelegated, change: Change::Up(hype) };
113 Ok(self.plan(action, CDeposit::KEY, effect, None))
114 }
115
116 pub fn unstake(&self, hype: Dec, summary: &DelegatorSummary) -> Result<Plan<CWithdraw>, Refused> {
119 let action = CWithdraw::new(hype, self.nonce)?;
120 at_most(hype, summary.undelegated)?;
121 if summary.n_pending_withdrawals >= MAX_PENDING_WITHDRAWALS {
122 return Err(Refused::QueueFull);
123 }
124 let effect = Effect { balance: Balance::Undelegated, change: Change::Down(hype) };
125 let ready = self.now_ms.saturating_add(unstaking_ms(self.network));
126 Ok(self.plan(action, CWithdraw::KEY, effect, Some(ready)))
127 }
128
129 pub fn delegate(
132 &self,
133 validator: Address,
134 hype: Dec,
135 summary: &DelegatorSummary,
136 ) -> Result<Plan<TokenDelegate>, Refused> {
137 let action = TokenDelegate::delegate(validator, hype, self.nonce)?;
138 at_most(hype, summary.undelegated)?;
139 let effect = Effect { balance: Balance::Undelegated, change: Change::Down(hype) };
140 Ok(self.plan(action, TokenDelegate::KEY, effect, Some(locked_until(self.network, self.now_ms))))
141 }
142
143 pub fn undelegate(
146 &self,
147 validator: Address,
148 hype: Dec,
149 delegations: &[Delegation],
150 ) -> Result<Plan<TokenDelegate>, Refused> {
151 let action = TokenDelegate::undelegate(validator, hype, self.nonce)?;
152 let held = delegations
153 .iter()
154 .find(|delegation| delegation.validator == validator)
155 .ok_or(Refused::NoDelegation(validator))?;
156 if held.is_locked(self.now_ms) {
157 return Err(Refused::Locked { until: held.locked_until_timestamp });
158 }
159 at_most(hype, held.amount)?;
160 let effect = Effect { balance: Balance::Undelegated, change: Change::Up(hype) };
161 Ok(self.plan(action, TokenDelegate::KEY, effect, None))
162 }
163}
164
165fn at_most(amount: Dec, held: Dec) -> Result<(), Refused> {
166 match amount > held {
167 true => Err(Refused::MoreThan { amount, held }),
168 false => Ok(()),
169 }
170}
171
172#[cfg(test)]
173#[path = "tests/plan.rs"]
174mod tests;