df_hyperliquid_core/earn/vaults/
plan.rs1use crate::account::Signers;
6use crate::client::Network;
7use crate::earn::vaults::reads::{VaultDetails, VaultEquity};
8use crate::earn::vaults::transfer::{VaultTransfer, lockup_ms};
9use crate::utils::{Address, BadUnits, Dec, USDC_DECIMALS, cut_units};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum Balance {
14 Vault(Address),
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Change {
23 Rises(Dec),
24 Falls(Dec),
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct Effect {
30 pub balance: Balance,
31 pub change: Change,
32}
33
34impl Effect {
35 #[must_use]
37 pub fn landed(&self, before: Dec, after: Dec) -> bool {
38 match self.change {
39 Change::Rises(_) => after > before,
40 Change::Falls(_) => after < before,
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Plan<A> {
48 pub network: Network,
49 pub action: A,
50 pub nonce: u64,
51 pub key: Signers,
52 pub effect: Effect,
53 pub free_at: Option<u64>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
59pub enum Refused {
60 #[error("{0} takes no deposits")]
61 NoDeposits(Address),
62 #[error("locked until {until} ms")]
63 Locked { until: u64 },
64 #[error("{amount} USDC is more than the {held} there is")]
65 MoreThan { amount: Dec, held: Dec },
66 #[error(transparent)]
67 Units(#[from] BadUnits),
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct Ctx {
73 pub network: Network,
74 pub nonce: u64,
75 pub now_ms: u64,
77}
78
79impl Ctx {
80 const fn plan(&self, action: VaultTransfer, change: Change, free_at: Option<u64>) -> Plan<VaultTransfer> {
81 let effect = Effect { balance: Balance::Vault(action.vault()), change };
82 Plan { network: self.network, action, nonce: self.nonce, key: VaultTransfer::KEY, effect, free_at }
83 }
84
85 pub fn deposit(&self, vault: &VaultDetails, usdc: Dec) -> Result<Plan<VaultTransfer>, Refused> {
87 let action = VaultTransfer::deposit(vault.vault_address, usdc)?;
88 if !vault.takes_deposits() {
89 return Err(Refused::NoDeposits(vault.vault_address));
90 }
91 let free_at = self.now_ms.saturating_add(lockup_ms(self.network, vault.vault_address));
92 Ok(self.plan(action, Change::Rises(usdc), Some(free_at)))
93 }
94
95 pub fn withdraw(&self, equity: &VaultEquity, usdc: Dec) -> Result<Plan<VaultTransfer>, Refused> {
97 let action = VaultTransfer::withdraw(equity.vault_address, usdc)?;
98 unlocked(equity, self.now_ms)?;
99 if usdc > equity.equity {
100 return Err(Refused::MoreThan { amount: usdc, held: equity.equity });
101 }
102 Ok(self.plan(action, Change::Falls(usdc), None))
103 }
104
105 pub fn withdraw_all(&self, equity: &VaultEquity) -> Result<Plan<VaultTransfer>, Refused> {
107 unlocked(equity, self.now_ms)?;
108 let usd = cut_units(equity.equity, USDC_DECIMALS)?;
109 let action = VaultTransfer::from_micros(equity.vault_address, false, usd);
110 let amount = equity.withdrawable(self.now_ms);
111 Ok(self.plan(action, Change::Falls(amount), None))
112 }
113}
114
115fn unlocked(equity: &VaultEquity, now_ms: u64) -> Result<(), Refused> {
116 match equity.is_locked(now_ms) {
117 true => Err(Refused::Locked { until: equity.locked_until_timestamp }),
118 false => Ok(()),
119 }
120}
121
122#[cfg(test)]
123#[path = "tests/plan.rs"]
124mod tests;