Skip to main content

df_hyperliquid_core/earn/vaults/
plan.rs

1//! From a vault move's typed values to exactly what is signed: the action,
2//! its nonce, the key it needs, the equity that should change once it lands,
3//! and when a deposit unlocks.
4
5use 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/// A figure a read back checks.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum Balance {
14    /// The account's equity in a vault: its `userVaultEquities` row. The perp
15    /// USDC on the other side moves by exactly the amount.
16    Vault(Address),
17}
18
19/// How the equity moves once the action lands. The vault's profit and loss
20/// moves it too, so a read back shows the direction, by about the amount.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Change {
23    Rises(Dec),
24    Falls(Dec),
25}
26
27/// The equity an action changes, and what reading it back should show.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct Effect {
30    pub balance: Balance,
31    pub change: Change,
32}
33
34impl Effect {
35    /// Whether `after`, read back, shows the action landed against `before`.
36    #[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/// Exactly what is signed, and what to check once it lands.
46#[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    /// When a deposit may first be withdrawn, milliseconds.
54    pub free_at: Option<u64>,
55}
56
57/// Why a move is not signed.
58#[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/// Everything outside the amounts that a plan is made from.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct Ctx {
73    pub network: Network,
74    pub nonce: u64,
75    /// Now, milliseconds: a lockup is measured from it.
76    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    /// USDC from the perp balance into `vault`, locked for its lockup.
86    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    /// `usdc` out of the vault `equity` is in, once its lockup has passed.
96    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    /// The whole equity, cut to millionths so it never asks for more.
106    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;