df_hyperliquid_core/earn/vaults/
plan.rs1use crate::account::signers::ActionKind;
6use crate::client::Network;
7use crate::earn::vaults::reads::{VaultDetails, VaultEquity};
8use crate::earn::vaults::transfer::{USD_DECIMALS, VaultTransfer, lockup_ms};
9use crate::utils::{Address, BadUnits, Dec, cut_units};
10
11pub use crate::account::signers::Signers as Key;
14
15const KINDS: [ActionKind; 1] = [ActionKind::VaultTransfer];
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 Vault(Address),
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Change {
36 Rises(Dec),
37 Falls(Dec),
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct Effect {
43 pub balance: Balance,
44 pub change: Change,
45}
46
47impl Effect {
48 #[must_use]
50 pub fn landed(&self, before: Dec, after: Dec) -> bool {
51 match self.change {
52 Change::Rises(_) => after > before,
53 Change::Falls(_) => after < before,
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Plan<A> {
61 pub network: Network,
62 pub action: A,
63 pub nonce: u64,
64 pub key: Key,
65 pub effect: Effect,
66 pub free_at: Option<u64>,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
72pub enum Refused {
73 #[error("{0} takes no deposits")]
74 NoDeposits(Address),
75 #[error("locked until {until} ms")]
76 Locked { until: u64 },
77 #[error("{amount} USDC is more than the {held} there is")]
78 MoreThan { amount: Dec, held: Dec },
79 #[error(transparent)]
80 Units(#[from] BadUnits),
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct Ctx {
86 pub network: Network,
87 pub nonce: u64,
88 pub now_ms: u64,
90}
91
92impl Ctx {
93 const fn plan(&self, action: VaultTransfer, change: Change, free_at: Option<u64>) -> Plan<VaultTransfer> {
94 let effect = Effect { balance: Balance::Vault(action.vault()), change };
95 Plan { network: self.network, action, nonce: self.nonce, key: VaultTransfer::KEY, effect, free_at }
96 }
97
98 pub fn deposit(&self, vault: &VaultDetails, usdc: Dec) -> Result<Plan<VaultTransfer>, Refused> {
100 let action = VaultTransfer::deposit(vault.vault_address, usdc)?;
101 if !vault.takes_deposits() {
102 return Err(Refused::NoDeposits(vault.vault_address));
103 }
104 let free_at = self.now_ms.saturating_add(lockup_ms(self.network, vault.vault_address));
105 Ok(self.plan(action, Change::Rises(usdc), Some(free_at)))
106 }
107
108 pub fn withdraw(&self, equity: &VaultEquity, usdc: Dec) -> Result<Plan<VaultTransfer>, Refused> {
110 let action = VaultTransfer::withdraw(equity.vault_address, usdc)?;
111 unlocked(equity, self.now_ms)?;
112 if usdc > equity.equity {
113 return Err(Refused::MoreThan { amount: usdc, held: equity.equity });
114 }
115 Ok(self.plan(action, Change::Falls(usdc), None))
116 }
117
118 pub fn withdraw_all(&self, equity: &VaultEquity) -> Result<Plan<VaultTransfer>, Refused> {
120 unlocked(equity, self.now_ms)?;
121 let usd = cut_units(equity.equity, USD_DECIMALS)?;
122 let action = VaultTransfer::from_micros(equity.vault_address, false, usd);
123 let amount = equity.withdrawable(self.now_ms);
124 Ok(self.plan(action, Change::Falls(amount), None))
125 }
126}
127
128fn unlocked(equity: &VaultEquity, now_ms: u64) -> Result<(), Refused> {
129 match equity.is_locked(now_ms) {
130 true => Err(Refused::Locked { until: equity.locked_until_timestamp }),
131 false => Ok(()),
132 }
133}
134
135#[cfg(test)]
136#[path = "tests/plan.rs"]
137mod tests;