1use std::{
17 collections::hash_map::DefaultHasher,
18 hash::{Hash, Hasher},
19 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
20};
21
22use serde_json::Value;
23
24use crate::{
25 common::enums::HyperliquidInfoRequestType,
26 http::{
27 models::HyperliquidExchangeAction,
28 query::{ExchangeAction, ExchangeActionParams, InfoRequest},
29 },
30};
31
32#[derive(Debug)]
33pub struct WeightedLimiter {
34 capacity: f64, refill_per_sec: f64, state: tokio::sync::Mutex<State>,
37}
38
39#[derive(Debug)]
40struct State {
41 tokens: f64,
42 last_refill: Instant,
43}
44
45impl WeightedLimiter {
46 pub fn per_minute(capacity: u32) -> Self {
47 let cap = capacity as f64;
48 Self {
49 capacity: cap,
50 refill_per_sec: cap / 60.0,
51 state: tokio::sync::Mutex::new(State {
52 tokens: cap,
53 last_refill: Instant::now(),
54 }),
55 }
56 }
57
58 pub async fn acquire(&self, weight: u32) {
60 let need = weight as f64;
61
62 loop {
63 let mut st = self.state.lock().await;
64 Self::refill_locked(&mut st, self.refill_per_sec, self.capacity);
65
66 if st.tokens >= need {
67 st.tokens -= need;
68 return;
69 }
70 let deficit = need - st.tokens;
71 let secs = deficit / self.refill_per_sec;
72 drop(st);
73 tokio::time::sleep(Duration::from_secs_f64(secs.max(0.01))).await;
74 }
75 }
76
77 pub async fn debit_extra(&self, extra: u32) {
79 if extra == 0 {
80 return;
81 }
82 let mut st = self.state.lock().await;
83 Self::refill_locked(&mut st, self.refill_per_sec, self.capacity);
84 st.tokens = (st.tokens - extra as f64).max(0.0);
85 }
86
87 pub async fn snapshot(&self) -> RateLimitSnapshot {
88 let mut st = self.state.lock().await;
89 Self::refill_locked(&mut st, self.refill_per_sec, self.capacity);
90 RateLimitSnapshot {
91 capacity: self.capacity as u32,
92 tokens: st.tokens.max(0.0) as u32,
93 }
94 }
95
96 fn refill_locked(st: &mut State, per_sec: f64, cap: f64) {
97 let dt = Instant::now().duration_since(st.last_refill).as_secs_f64();
98 if dt > 0.0 {
99 st.tokens = (st.tokens + dt * per_sec).min(cap);
100 st.last_refill = Instant::now();
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy)]
106pub struct RateLimitSnapshot {
107 pub capacity: u32,
108 pub tokens: u32,
109}
110
111pub fn backoff_full_jitter(attempt: u32, base: Duration, cap: Duration) -> Duration {
112 let mut hasher = DefaultHasher::new();
113 attempt.hash(&mut hasher);
114 let nanos = SystemTime::now()
115 .duration_since(UNIX_EPOCH)
116 .unwrap_or_default()
117 .as_nanos();
118 nanos.hash(&mut hasher);
119 let hash = hasher.finish();
120
121 let max = (base.as_millis() as u64)
122 .saturating_mul(1u64 << attempt.min(16))
123 .min(cap.as_millis() as u64)
124 .max(base.as_millis() as u64);
125
126 Duration::from_millis((hash % max).max(1))
128}
129
130pub fn info_base_weight(req: &InfoRequest) -> u32 {
132 match req.request_type {
133 HyperliquidInfoRequestType::L2Book
134 | HyperliquidInfoRequestType::AllMids
135 | HyperliquidInfoRequestType::RecentTrades
136 | HyperliquidInfoRequestType::ClearinghouseState
137 | HyperliquidInfoRequestType::OrderStatus
138 | HyperliquidInfoRequestType::SpotClearinghouseState
139 | HyperliquidInfoRequestType::ExchangeStatus
140 | HyperliquidInfoRequestType::UserFees => 2,
141 HyperliquidInfoRequestType::UserRole => 60,
142 _ => 20,
143 }
144}
145
146pub fn info_extra_weight(req: &InfoRequest, json: &Value) -> u32 {
149 let items = match json {
150 Value::Array(a) => a.len(),
151 Value::Object(m) => m
152 .values()
153 .filter_map(|v| v.as_array().map(|a| a.len()))
154 .max()
155 .unwrap_or(0),
156 _ => 0,
157 };
158
159 let unit = match req.request_type {
160 HyperliquidInfoRequestType::CandleSnapshot => 60usize,
161 HyperliquidInfoRequestType::HistoricalOrders
162 | HyperliquidInfoRequestType::UserFills
163 | HyperliquidInfoRequestType::UserFillsByTime
164 | HyperliquidInfoRequestType::FundingHistory
165 | HyperliquidInfoRequestType::UserFunding
166 | HyperliquidInfoRequestType::NonUserFundingUpdates
167 | HyperliquidInfoRequestType::TwapHistory
168 | HyperliquidInfoRequestType::UserTwapSliceFills
169 | HyperliquidInfoRequestType::UserTwapSliceFillsByTime
170 | HyperliquidInfoRequestType::DelegatorHistory
171 | HyperliquidInfoRequestType::DelegatorRewards
172 | HyperliquidInfoRequestType::ValidatorStats => 20usize,
173 _ => return 0,
174 };
175 (items / unit) as u32
176}
177
178pub fn exchange_weight(action: &ExchangeAction) -> u32 {
180 let batch_size = match &action.params {
182 ExchangeActionParams::Order(params) => params.orders.len(),
183 ExchangeActionParams::Cancel(params) => params.cancels.len(),
184 ExchangeActionParams::Modify(_) => {
185 1
187 }
188 ExchangeActionParams::UpdateLeverage(_) | ExchangeActionParams::UpdateIsolatedMargin(_) => {
189 0
190 }
191 };
192 1 + (batch_size as u32 / 40)
193}
194
195pub fn exec_action_weight(action: &HyperliquidExchangeAction) -> u32 {
197 let batch_size = match action {
198 HyperliquidExchangeAction::Order { orders, .. } => orders.len(),
199 HyperliquidExchangeAction::Cancel { cancels, .. } => cancels.len(),
200 HyperliquidExchangeAction::CancelByCloid { cancels, .. } => cancels.len(),
201 HyperliquidExchangeAction::Modify { .. } => 1,
202 HyperliquidExchangeAction::BatchModify { modifies } => modifies.len(),
203 HyperliquidExchangeAction::UpdateLeverage { .. }
204 | HyperliquidExchangeAction::UpdateIsolatedMargin { .. }
205 | HyperliquidExchangeAction::ScheduleCancel { .. }
206 | HyperliquidExchangeAction::UsdClassTransfer { .. }
207 | HyperliquidExchangeAction::UserOutcome { .. }
208 | HyperliquidExchangeAction::TwapPlace { .. }
209 | HyperliquidExchangeAction::TwapCancel { .. }
210 | HyperliquidExchangeAction::Noop => 0,
211 };
212 1 + (batch_size as u32 / 40)
213}
214
215#[cfg(test)]
216mod tests {
217 use rstest::rstest;
218 use rust_decimal::Decimal;
219
220 use super::{
221 super::models::{
222 Cloid, HyperliquidExchangeAction, HyperliquidExchangeCancelByCloidRequest,
223 HyperliquidExchangeCancelOrderRequest, HyperliquidExchangeGrouping,
224 HyperliquidExchangeLimitParams, HyperliquidExchangeModifyOrderRequest,
225 HyperliquidExchangeOrderKind, HyperliquidExchangePlaceOrderRequest,
226 HyperliquidExchangeTif,
227 },
228 *,
229 };
230 use crate::http::query::{
231 CancelParams, ExchangeAction, ExchangeActionParams, ExchangeActionType, OrderParams,
232 UpdateLeverageParams,
233 };
234
235 fn exec_order() -> HyperliquidExchangePlaceOrderRequest {
236 HyperliquidExchangePlaceOrderRequest {
237 asset: 0,
238 is_buy: true,
239 price: Decimal::new(50000, 0),
240 size: Decimal::new(1, 0),
241 reduce_only: false,
242 kind: HyperliquidExchangeOrderKind::Limit {
243 limit: HyperliquidExchangeLimitParams {
244 tif: HyperliquidExchangeTif::Gtc,
245 },
246 },
247 cloid: Some(Cloid::from_hex("0x00000000000000000000000000000000").unwrap()),
248 }
249 }
250
251 fn exec_modify() -> HyperliquidExchangeModifyOrderRequest {
252 HyperliquidExchangeModifyOrderRequest {
253 oid: 12345.into(),
254 order: exec_order(),
255 }
256 }
257
258 fn exec_cancel_by_cloid() -> HyperliquidExchangeCancelByCloidRequest {
259 HyperliquidExchangeCancelByCloidRequest {
260 asset: 0,
261 cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
262 }
263 }
264
265 #[rstest]
266 #[case(1, 1)]
267 #[case(39, 1)]
268 #[case(40, 2)]
269 #[case(79, 2)]
270 #[case(80, 3)]
271 fn test_exchange_weight_order_steps_every_40(
272 #[case] array_len: usize,
273 #[case] expected_weight: u32,
274 ) {
275 let orders: Vec<HyperliquidExchangePlaceOrderRequest> =
276 (0..array_len).map(|_| exec_order()).collect();
277
278 let action = ExchangeAction {
279 action_type: ExchangeActionType::Order,
280 params: ExchangeActionParams::Order(OrderParams {
281 orders,
282 grouping: HyperliquidExchangeGrouping::Na,
283 builder: None,
284 }),
285 };
286 assert_eq!(exchange_weight(&action), expected_weight);
287 }
288
289 #[rstest]
290 #[case(1, 1)]
291 #[case(39, 1)]
292 #[case(40, 2)]
293 #[case(79, 2)]
294 #[case(80, 3)]
295 fn test_exec_action_weight_order_steps_every_40(
296 #[case] array_len: usize,
297 #[case] expected_weight: u32,
298 ) {
299 let action = HyperliquidExchangeAction::Order {
300 orders: (0..array_len).map(|_| exec_order()).collect(),
301 grouping: HyperliquidExchangeGrouping::Na,
302 builder: None,
303 };
304
305 assert_eq!(exec_action_weight(&action), expected_weight);
306 }
307
308 #[rstest]
309 #[case(1, 1)]
310 #[case(39, 1)]
311 #[case(40, 2)]
312 #[case(79, 2)]
313 #[case(80, 3)]
314 fn test_exec_action_weight_cancel_by_oid_steps_every_40(
315 #[case] array_len: usize,
316 #[case] expected_weight: u32,
317 ) {
318 let action = HyperliquidExchangeAction::Cancel {
319 cancels: (0..array_len)
320 .map(|i| HyperliquidExchangeCancelOrderRequest {
321 asset: 0,
322 oid: i as u64,
323 })
324 .collect(),
325 fast: None,
326 };
327
328 assert_eq!(exec_action_weight(&action), expected_weight);
329 }
330
331 #[rstest]
332 #[case(1, 1)]
333 #[case(39, 1)]
334 #[case(40, 2)]
335 #[case(79, 2)]
336 #[case(80, 3)]
337 fn test_exec_action_weight_cancel_by_cloid_steps_every_40(
338 #[case] array_len: usize,
339 #[case] expected_weight: u32,
340 ) {
341 let action = HyperliquidExchangeAction::CancelByCloid {
342 cancels: (0..array_len).map(|_| exec_cancel_by_cloid()).collect(),
343 fast: None,
344 };
345
346 assert_eq!(exec_action_weight(&action), expected_weight);
347 }
348
349 #[rstest]
350 #[case(1, 1)]
351 #[case(39, 1)]
352 #[case(40, 2)]
353 #[case(79, 2)]
354 #[case(80, 3)]
355 fn test_exec_action_weight_batch_modify_steps_every_40(
356 #[case] array_len: usize,
357 #[case] expected_weight: u32,
358 ) {
359 let action = HyperliquidExchangeAction::BatchModify {
360 modifies: (0..array_len).map(|_| exec_modify()).collect(),
361 };
362
363 assert_eq!(exec_action_weight(&action), expected_weight);
364 }
365
366 #[rstest]
367 fn test_exec_action_weight_modify() {
368 let action = HyperliquidExchangeAction::Modify {
369 modify: exec_modify(),
370 };
371
372 assert_eq!(exec_action_weight(&action), 1);
373 }
374
375 #[rstest]
376 fn test_exec_action_weight_non_batch_action() {
377 let action = HyperliquidExchangeAction::UpdateLeverage {
378 asset: 1,
379 is_cross: true,
380 leverage: 10,
381 };
382
383 assert_eq!(exec_action_weight(&action), 1);
384 }
385
386 #[rstest]
387 fn test_exchange_weight_cancel() {
388 let cancels: Vec<HyperliquidExchangeCancelByCloidRequest> =
389 (0..40).map(|_| exec_cancel_by_cloid()).collect();
390
391 let action = ExchangeAction {
392 action_type: ExchangeActionType::Cancel,
393 params: ExchangeActionParams::Cancel(CancelParams {
394 cancels,
395 fast: None,
396 }),
397 };
398 assert_eq!(exchange_weight(&action), 2);
399 }
400
401 #[rstest]
402 fn test_exchange_weight_non_batch_action() {
403 let update_leverage = ExchangeAction {
404 action_type: ExchangeActionType::UpdateLeverage,
405 params: ExchangeActionParams::UpdateLeverage(UpdateLeverageParams {
406 asset: 1,
407 is_cross: true,
408 leverage: 10,
409 }),
410 };
411 assert_eq!(exchange_weight(&update_leverage), 1);
412 }
413
414 #[tokio::test]
415 async fn test_limiter_roughly_caps_to_capacity() {
416 let limiter = WeightedLimiter::per_minute(1200);
417
418 for _ in 0..60 {
420 limiter.acquire(20).await; }
422
423 let t0 = std::time::Instant::now();
425 limiter.acquire(20).await;
426 let elapsed = t0.elapsed();
427
428 assert!(
430 elapsed.as_millis() >= 500,
431 "Expected significant delay, was {}ms",
432 elapsed.as_millis()
433 );
434 }
435
436 #[tokio::test]
437 async fn test_limiter_debit_extra_works() {
438 let limiter = WeightedLimiter::per_minute(100);
439
440 let snapshot = limiter.snapshot().await;
442 assert_eq!(snapshot.capacity, 100);
443 assert_eq!(snapshot.tokens, 100);
444
445 limiter.acquire(30).await;
447 let snapshot = limiter.snapshot().await;
448 assert_eq!(snapshot.tokens, 70);
449
450 limiter.debit_extra(20).await;
452 let snapshot = limiter.snapshot().await;
453 assert_eq!(snapshot.tokens, 50);
454
455 limiter.debit_extra(100).await;
457 let snapshot = limiter.snapshot().await;
458 assert_eq!(snapshot.tokens, 0);
459 }
460
461 #[rstest]
462 #[case(0, 100)]
463 #[case(1, 200)]
464 #[case(2, 400)]
465 fn test_backoff_full_jitter_increases(#[case] attempt: u32, #[case] max_expected_ms: u64) {
466 let base = Duration::from_millis(100);
467 let cap = Duration::from_secs(5);
468
469 let delay = backoff_full_jitter(attempt, base, cap);
470
471 assert!(delay.as_millis() >= 1);
472 assert!(delay.as_millis() <= max_expected_ms as u128);
473 }
474
475 #[rstest]
476 fn test_backoff_full_jitter_respects_cap() {
477 let base = Duration::from_millis(100);
478 let cap = Duration::from_secs(5);
479
480 let delay_high = backoff_full_jitter(10, base, cap);
481 assert!(delay_high.as_millis() <= cap.as_millis());
482 }
483}