df_hyperliquid_core/client/
limit.rs1use std::collections::VecDeque;
6
7pub const LIMIT: u32 = 1_200;
9
10pub const SOFT: u32 = LIMIT / 5 * 4;
13
14pub const WINDOW_MS: u64 = 60_000;
16
17pub const BASE_COOLDOWN_MS: u64 = 2_000;
19
20pub const MAX_COOLDOWN_MS: u64 = 60_000;
22
23const MAX_STRIKES: u32 = 6;
24
25#[must_use]
27pub fn base_weight(kind: &str) -> u32 {
28 match kind {
29 "l2Book" | "allMids" | "clearinghouseState" | "orderStatus" | "spotClearinghouseState" | "exchangeStatus" => 2,
30 "userRole" => 60,
31 _ => 20,
32 }
33}
34
35#[must_use]
38pub fn per_items(kind: &str) -> Option<usize> {
39 match kind {
40 "recentTrades"
41 | "historicalOrders"
42 | "userFills"
43 | "userFillsByTime"
44 | "fundingHistory"
45 | "userFunding"
46 | "nonUserFundingUpdates"
47 | "twapHistory"
48 | "userTwapSliceFills"
49 | "userTwapSliceFillsByTime"
50 | "delegatorHistory"
51 | "delegatorRewards"
52 | "validatorStats" => Some(20),
53 "candleSnapshot" => Some(60),
54 _ => None,
55 }
56}
57
58#[must_use]
60pub fn read_weight(kind: &str, items: usize) -> u32 {
61 let extra = per_items(kind).map_or(0, |per| items / per);
62 base_weight(kind).saturating_add(u32::try_from(extra).unwrap_or(u32::MAX))
63}
64
65#[must_use]
67pub fn action_weight(batched: usize) -> u32 {
68 1u32.saturating_add(u32::try_from(batched / 40).unwrap_or(u32::MAX))
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct Spent {
74 pub limit: u32,
75 pub minute: u32,
76 pub kinds: Vec<Kind>,
78 pub refused: u32,
80 pub cooldown_ms: u64,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct Kind {
86 pub name: String,
87 pub count: u32,
88 pub weight: u32,
89}
90
91#[derive(Debug, Default)]
94pub struct Meter {
95 sent: VecDeque<(u64, String, u32)>,
96 refused: VecDeque<u64>,
97}
98
99impl Meter {
100 fn prune(&mut self, now: u64) {
102 let gone = |at: u64| at.saturating_add(WINDOW_MS) <= now;
103 while self.sent.front().is_some_and(|(at, _, _)| gone(*at)) {
104 self.sent.pop_front();
105 }
106 while self.refused.front().is_some_and(|at| gone(*at)) {
107 self.refused.pop_front();
108 }
109 }
110
111 pub fn record(&mut self, now: u64, kind: &str, weight: u32) {
112 self.prune(now);
113 self.sent.push_back((now, kind.to_owned(), weight));
114 }
115
116 pub fn refuse(&mut self, now: u64) {
117 self.prune(now);
118 self.refused.push_back(now);
119 }
120
121 pub fn wait_for(&mut self, now: u64, need: u32) -> Option<u64> {
124 self.prune(now);
125 let spent = self.sent.iter().fold(0u32, |sum, (_, _, weight)| sum.saturating_add(*weight));
126 let mut over = spent.saturating_add(need).checked_sub(SOFT).filter(|over| *over > 0)?;
127 for (at, _, weight) in &self.sent {
128 if *weight >= over {
129 return Some(at.saturating_add(WINDOW_MS).saturating_sub(now).max(1));
130 }
131 over -= weight;
132 }
133 Some(WINDOW_MS)
134 }
135
136 pub fn spent(&mut self, now: u64) -> Spent {
138 self.prune(now);
139 let mut kinds: Vec<Kind> = Vec::new();
140 for (_, name, weight) in &self.sent {
141 match kinds.iter_mut().find(|kind| kind.name == *name) {
142 Some(kind) => {
143 kind.count += 1;
144 kind.weight = kind.weight.saturating_add(*weight);
145 }
146 None => kinds.push(Kind { name: name.clone(), count: 1, weight: *weight }),
147 }
148 }
149 kinds.sort_by(|a, b| b.weight.cmp(&a.weight).then_with(|| a.name.cmp(&b.name)));
150 let minute = kinds.iter().fold(0u32, |sum, kind| sum.saturating_add(kind.weight));
151 Spent {
152 limit: LIMIT,
153 minute,
154 kinds,
155 refused: u32::try_from(self.refused.len()).unwrap_or(u32::MAX),
156 cooldown_ms: 0,
157 }
158 }
159}
160
161#[derive(Debug, Clone, Copy)]
164pub struct Ticket(u64);
165
166#[derive(Debug, Default)]
169pub struct Gate {
170 until: u64,
171 strikes: u32,
173 epoch: u64,
175}
176
177impl Gate {
178 pub fn open(&self, now: u64) -> Result<Ticket, u64> {
180 match self.until > now {
181 true => Err(self.until - now),
182 false => Ok(Ticket(self.epoch)),
183 }
184 }
185
186 pub fn passed(&mut self, ticket: Ticket) {
189 if ticket.0 == self.epoch {
190 self.strikes = 0;
191 }
192 }
193
194 pub fn struck(&mut self, ticket: Ticket, now: u64, retry_after_ms: Option<u64>) -> u64 {
198 let wait = match ticket.0 == self.epoch {
199 true => {
200 self.epoch += 1;
201 self.strikes = (self.strikes + 1).min(MAX_STRIKES);
202 let backoff = (BASE_COOLDOWN_MS << (self.strikes - 1)).min(MAX_COOLDOWN_MS);
203 backoff.max(retry_after_ms.unwrap_or(0))
204 }
205 false => retry_after_ms.unwrap_or(0),
206 };
207 self.until = self.until.max(now.saturating_add(wait));
208 self.until.saturating_sub(now)
209 }
210
211 #[must_use]
212 pub const fn cooldown_ms(&self, now: u64) -> u64 {
213 self.until.saturating_sub(now)
214 }
215}
216
217#[cfg(test)]
218#[path = "tests/limit.rs"]
219mod tests;