Skip to main content

df_hyperliquid_core/earn/lending/
state.rs

1//! `allBorrowLendReserveStates`, `borrowLendReserveState` and
2//! `borrowLendUserState`. Figures are the venue's decimal strings; rates and
3//! utilization are fractions (0.05 is 5%). A token is its spot index, from the
4//! caller's `spotMeta`: HYPE is 150 on mainnet and 1105 on testnet.
5
6use crate::client::{ApiError, Transport, info, info_request, user_request};
7use crate::utils::{Address, Dec, Figure, Json};
8use serde::Deserialize;
9
10/// `{"type":"allBorrowLendReserveStates"}`.
11#[must_use]
12pub fn all_reserve_states_request() -> String {
13    info_request("allBorrowLendReserveStates", &[])
14}
15
16/// `{"type":"borrowLendReserveState","token":..}`. A token with no reserve is
17/// answered HTTP 500.
18#[must_use]
19pub fn reserve_state_request(token: u32) -> String {
20    info_request("borrowLendReserveState", &[("token", Json::Number(token.to_string()))])
21}
22
23/// `{"type":"borrowLendUserState","user":..}`.
24#[must_use]
25pub fn user_state_request(user: Address) -> String {
26    user_request("borrowLendUserState", user)
27}
28
29/// Every reserve, as `[token index, state]` pairs.
30pub async fn all_reserve_states(transport: &impl Transport) -> Result<Vec<(u32, ReserveState)>, ApiError> {
31    info(transport, all_reserve_states_request()).await
32}
33
34/// One token's reserve.
35pub async fn reserve_state(transport: &impl Transport, token: u32) -> Result<ReserveState, ApiError> {
36    info(transport, reserve_state_request(token)).await
37}
38
39pub async fn user_state(transport: &impl Transport, user: Address) -> Result<UserState, ApiError> {
40    info(transport, user_state_request(user)).await
41}
42
43/// One token's reserve.
44#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct ReserveState {
47    /// What a borrower pays a year.
48    pub borrow_yearly_rate: Dec,
49    /// What a supplier earns a year.
50    pub supply_yearly_rate: Dec,
51    /// The reserve's balance, in the token: what it can still lend. Live it is
52    /// near, but not exactly, supplied less borrowed.
53    pub balance: Dec,
54    /// Borrowed over supplied.
55    pub utilization: Dec,
56    /// In USDC.
57    pub oracle_px: Dec,
58    /// How much of the token's value backs a borrow; 0 where it backs none.
59    pub ltv: Dec,
60    pub total_supplied: Dec,
61    pub total_borrowed: Dec,
62}
63
64/// The reserve of `token` among `reserves`.
65#[must_use]
66pub fn reserve(reserves: &[(u32, ReserveState)], token: u32) -> Option<&ReserveState> {
67    reserves.iter().find(|(index, _)| *index == token).map(|(_, reserve)| reserve)
68}
69
70/// An account's lending: a position per token it ever touched, and its
71/// health.
72#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct UserState {
75    /// `[token index, position]` pairs; zero rows stay listed.
76    #[serde(default)]
77    pub token_to_state: Vec<(u32, Position)>,
78    /// The venue's word: `healthy`, `atRisk` (seen live on a portfolio-margin
79    /// account), or `liquidatable`.
80    pub health: String,
81    /// `null` while nothing is borrowed.
82    #[serde(default)]
83    pub health_factor: Option<Figure>,
84}
85
86/// How near liquidation the venue says an account's lending is.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
88pub enum Risk {
89    Safe,
90    /// `atRisk`, or a word not seen yet: worth a warning.
91    Near,
92    Liquidatable,
93}
94
95impl UserState {
96    #[must_use]
97    pub fn position(&self, token: u32) -> Option<&Position> {
98        self.token_to_state.iter().find(|(index, _)| *index == token).map(|(_, position)| position)
99    }
100
101    /// The positions that hold or owe anything.
102    pub fn positions(&self) -> impl Iterator<Item = (u32, &Position)> {
103        self.token_to_state
104            .iter()
105            .filter(|(_, position)| !position.supply.value.is_zero() || !position.borrow.value.is_zero())
106            .map(|(token, position)| (*token, position))
107    }
108
109    /// What is supplied of `token` now, interest in; zero for none.
110    #[must_use]
111    pub fn supplied(&self, token: u32) -> Dec {
112        self.position(token).map_or(Dec::ZERO, |position| position.supply.value)
113    }
114
115    /// What is owed of `token` now, interest in; zero for none.
116    #[must_use]
117    pub fn borrowed(&self, token: u32) -> Dec {
118        self.position(token).map_or(Dec::ZERO, |position| position.borrow.value)
119    }
120
121    #[must_use]
122    pub fn is_healthy(&self) -> bool {
123        self.health == "healthy"
124    }
125
126    /// The venue's word as a [`Risk`]: any word other than `healthy` or
127    /// `liquidatable` reads as [`Risk::Near`].
128    #[must_use]
129    pub fn risk(&self) -> Risk {
130        match self.health.as_str() {
131            "healthy" => Risk::Safe,
132            "liquidatable" => Risk::Liquidatable,
133            _ => Risk::Near,
134        }
135    }
136
137    /// The health factor as written, while something is borrowed.
138    #[must_use]
139    pub fn factor(&self) -> Option<Dec> {
140        self.health_factor.as_ref().and_then(Figure::to_dec)
141    }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
145pub struct Position {
146    #[serde(default)]
147    pub borrow: Basis,
148    #[serde(default)]
149    pub supply: Basis,
150}
151
152/// `value` is what is held or owed now, in the token; `basis` what went in.
153/// A live portfolio-margin account wrote positions with no `basis`.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
155pub struct Basis {
156    #[serde(default)]
157    pub basis: Dec,
158    pub value: Dec,
159}
160
161impl Basis {
162    /// The interest earned or owed so far: the value less the basis.
163    #[must_use]
164    pub fn interest(&self) -> Option<Dec> {
165        self.value.checked_sub(self.basis)
166    }
167}
168
169#[cfg(test)]
170#[path = "tests/lend.rs"]
171mod tests;