Skip to main content

df_hyperliquid_core/earn/lending/
plan.rs

1//! From a lending move's typed values to exactly what is signed: the action,
2//! its nonce, the key it needs, and the position that should change once it
3//! lands. A token is its spot index from the caller's `spotMeta`.
4
5use crate::account::Signers;
6use crate::client::Network;
7use crate::earn::lending::action::{BadLend, BorrowLend};
8use crate::earn::lending::state::{ReserveState, UserState, reserve};
9use crate::utils::Dec;
10
11/// A figure a read back checks: a position's `value` in
12/// `borrowLendUserState`, by token.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum Balance {
15    Supplied(u32),
16    Borrowed(u32),
17}
18
19/// How the position moves once the action lands. Interest moves it too, so a
20/// 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    /// To nothing: all of it withdrawn or repaid.
26    Empties,
27}
28
29/// The position an action changes, and what reading it back should show.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct Effect {
32    pub balance: Balance,
33    pub change: Change,
34}
35
36impl Effect {
37    /// Whether `after`, read back, shows the action landed against `before`.
38    #[must_use]
39    pub fn landed(&self, before: Dec, after: Dec) -> bool {
40        match self.change {
41            Change::Rises(_) => after > before,
42            Change::Falls(_) => after < before,
43            Change::Empties => after.is_zero(),
44        }
45    }
46}
47
48/// Exactly what is signed, and what to check once it lands.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Plan<A> {
51    pub network: Network,
52    pub action: A,
53    pub nonce: u64,
54    pub key: Signers,
55    pub effect: Effect,
56}
57
58/// Why a move is not signed.
59#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
60pub enum Refused {
61    #[error("token {0} has no lending reserve")]
62    NoReserve(u32),
63    #[error("{amount} is more than the {held} there is")]
64    MoreThan { amount: Dec, held: Dec },
65    #[error("nothing of token {0} is there to take back")]
66    Nothing(u32),
67    #[error(transparent)]
68    Lend(#[from] BadLend),
69}
70
71/// Everything outside the amounts that a plan is made from.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct Ctx {
74    pub network: Network,
75    pub nonce: u64,
76}
77
78impl Ctx {
79    const fn plan(&self, action: BorrowLend, effect: Effect) -> Plan<BorrowLend> {
80        Plan { network: self.network, action, nonce: self.nonce, key: BorrowLend::KEY, effect }
81    }
82
83    /// Supplies `amount` of `token`, already cut to its `weiDecimals`.
84    pub fn supply(
85        &self,
86        token: u32,
87        amount: Dec,
88        reserves: &[(u32, ReserveState)],
89    ) -> Result<Plan<BorrowLend>, Refused> {
90        let action = BorrowLend::supply(token, amount)?;
91        reserve(reserves, token).ok_or(Refused::NoReserve(token))?;
92        Ok(self.plan(action, Effect { balance: Balance::Supplied(token), change: Change::Rises(amount) }))
93    }
94
95    /// Takes back `amount` of what is supplied, or with `None` all of it.
96    pub fn withdraw(&self, token: u32, amount: Option<Dec>, state: &UserState) -> Result<Plan<BorrowLend>, Refused> {
97        let held = state.supplied(token);
98        let (action, change) = back(token, amount, held, BorrowLend::withdraw, BorrowLend::withdraw_all)?;
99        Ok(self.plan(action, Effect { balance: Balance::Supplied(token), change }))
100    }
101
102    /// Borrows `amount` of `token`, at most what its reserve can lend.
103    pub fn borrow(
104        &self,
105        token: u32,
106        amount: Dec,
107        reserves: &[(u32, ReserveState)],
108    ) -> Result<Plan<BorrowLend>, Refused> {
109        let action = BorrowLend::borrow(token, amount)?;
110        let lendable = reserve(reserves, token).ok_or(Refused::NoReserve(token))?.lendable();
111        if amount > lendable {
112            return Err(Refused::MoreThan { amount, held: lendable });
113        }
114        Ok(self.plan(action, Effect { balance: Balance::Borrowed(token), change: Change::Rises(amount) }))
115    }
116
117    /// Repays `amount` of what is owed, or with `None` all of it, interest in.
118    pub fn repay(&self, token: u32, amount: Option<Dec>, state: &UserState) -> Result<Plan<BorrowLend>, Refused> {
119        let held = state.borrowed(token);
120        let (action, change) = back(token, amount, held, BorrowLend::repay, BorrowLend::repay_all)?;
121        Ok(self.plan(action, Effect { balance: Balance::Borrowed(token), change }))
122    }
123}
124
125/// A withdraw or repay of `amount` against `held`, or of all of it.
126fn back(
127    token: u32,
128    amount: Option<Dec>,
129    held: Dec,
130    some: fn(u32, Dec) -> Result<BorrowLend, BadLend>,
131    all: fn(u32) -> BorrowLend,
132) -> Result<(BorrowLend, Change), Refused> {
133    if !held.is_positive() {
134        return Err(Refused::Nothing(token));
135    }
136    match amount {
137        None => Ok((all(token), Change::Empties)),
138        Some(amount) => {
139            let action = some(token, amount)?;
140            match amount > held {
141                true => Err(Refused::MoreThan { amount, held }),
142                false => Ok((action, Change::Falls(amount))),
143            }
144        }
145    }
146}
147
148#[cfg(test)]
149#[path = "tests/plan.rs"]
150mod tests;