df_hyperliquid_core/account/
limits.rs1use crate::client::{ApiError, Transport, info, user_request};
6use crate::utils::{Address, Dec, Round};
7use serde::Deserialize;
8
9pub const INITIAL_REQUESTS: u64 = 10_000;
11
12pub const THROTTLED_EVERY_MS: u64 = 10_000;
14
15pub const OPEN_ORDERS: u64 = 1_000;
17pub const MOST_OPEN_ORDERS: u64 = 5_000;
19
20pub const VOLUME_PER_OPEN_ORDER: i64 = 5_000_000;
22
23pub const READ_EVERY_MS: u64 = 60_000;
26
27pub const READ_BATCH: usize = 10;
30
31#[must_use]
33pub fn rate_limit_request(user: Address) -> String {
34 user_request("userRateLimit", user)
35}
36
37pub async fn rate_limit(transport: &impl Transport, user: Address) -> Result<RateLimit, ApiError> {
39 info(transport, rate_limit_request(user)).await
40}
41
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
44pub struct RateLimit {
45 #[serde(rename = "cumVlm")]
47 pub volume: Dec,
48 #[serde(rename = "nRequestsUsed")]
49 pub used: u64,
50 #[serde(rename = "nRequestsCap")]
51 pub cap: u64,
52 #[serde(rename = "nRequestsSurplus", default)]
54 pub surplus: u64,
55}
56
57impl RateLimit {
58 #[must_use]
61 pub const fn remaining(&self) -> u64 {
62 self.cap.saturating_sub(self.used)
63 }
64
65 #[must_use]
66 pub const fn is_throttled(&self) -> bool {
67 self.remaining() == 0
68 }
69
70 #[must_use]
72 pub const fn cancel_cap(&self) -> u64 {
73 let wider = self.cap.saturating_add(100_000);
74 let doubled = self.cap.saturating_mul(2);
75 if wider < doubled { wider } else { doubled }
76 }
77
78 #[must_use]
81 pub fn earned_cap(volume: Dec) -> Option<u64> {
82 let whole = u64::try_from(volume.units_at(0, Round::Down)?).ok()?;
83 INITIAL_REQUESTS.checked_add(whole)
84 }
85}
86
87#[must_use]
89pub fn volume_for(requests: u64) -> Dec {
90 Dec::lit::<0>(i128::from(requests))
91}
92
93#[must_use]
96pub fn surplus_cost(requests: u64) -> Option<Dec> {
97 Dec::lit::<0>(i128::from(requests)).checked_mul(Dec::lit::<4>(5))
98}
99
100#[must_use]
103pub fn address_requests(batched: usize) -> u64 {
104 u64::try_from(batched.max(1)).unwrap_or(u64::MAX)
105}
106
107#[must_use]
109pub fn open_order_cap(volume: Dec) -> u64 {
110 let earned = volume
111 .checked_div(Dec::from_int(VOLUME_PER_OPEN_ORDER), 0, Round::Down)
112 .and_then(|earned| earned.units_at(0, Round::Down))
113 .and_then(|earned| u64::try_from(earned).ok())
114 .unwrap_or(0);
115 OPEN_ORDERS.saturating_add(earned).min(MOST_OPEN_ORDERS)
116}
117
118#[must_use]
121pub fn read_wait_ms(read_at: Option<u64>, now: u64) -> Option<u64> {
122 let next = read_at?.saturating_add(READ_EVERY_MS);
123 (next > now).then(|| next - now)
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub struct Sample {
129 pub at: u64,
130 pub limit: RateLimit,
131}
132
133#[derive(Debug, Clone, Default, PartialEq, Eq)]
135pub struct Budget {
136 pub last: Option<Sample>,
137 pub base: Option<Sample>,
139 pub error: Option<String>,
140}
141
142impl Budget {
143 pub fn took(&mut self, sample: Sample) {
144 if let Some(last) = self.last
145 && sample.at.saturating_sub(last.at) >= READ_EVERY_MS
146 {
147 self.base = Some(last);
148 }
149 self.last = Some(sample);
150 self.error = None;
151 }
152
153 #[must_use]
154 pub fn limit(&self) -> Option<RateLimit> {
155 self.last.map(|sample| sample.limit)
156 }
157
158 fn spent(&self) -> Option<(i128, i128)> {
161 let (base, last) = (self.base?, self.last?);
162 let ms = i128::from(last.at) - i128::from(base.at);
163 if ms <= 0 {
164 return None;
165 }
166 let used = i128::from(last.limit.used) - i128::from(base.limit.used);
167 let earned = i128::from(last.limit.cap) - i128::from(base.limit.cap);
168 Some((used - earned, ms))
169 }
170
171 #[must_use]
174 pub fn net_a_minute(&self) -> Option<i64> {
175 let (net, ms) = self.spent()?;
176 i64::try_from(net * 60_000 / ms).ok()
177 }
178
179 #[must_use]
182 pub fn runs_out_ms(&self) -> Option<u64> {
183 let (net, ms) = self.spent()?;
184 if net <= 0 {
185 return None;
186 }
187 let left = i128::from(self.limit()?.remaining());
188 u64::try_from(left * ms / net).ok()
189 }
190}
191
192#[cfg(test)]
193#[path = "tests/limits.rs"]
194mod tests;