df_hyperliquid_core/client/
api.rs1use std::sync::{Mutex, MutexGuard, PoisonError};
6
7use crate::client::limit::{Gate, Meter, Spent, action_weight, base_weight, per_items, read_weight};
8use crate::client::network::Network;
9use crate::client::transport::{Http, INFO, Request, Transport, TransportError};
10use crate::utils::json::Json;
11
12const EXCHANGE_KIND: &str = "exchange";
14
15pub type Clock = fn() -> u64;
18
19#[derive(Debug, Default)]
20struct Limiter {
21 gate: Gate,
22 meter: Meter,
23}
24
25pub struct Api<H> {
27 http: H,
28 network: Network,
29 clock: Clock,
30 limiter: Mutex<Limiter>,
31}
32
33impl<H: Http> Api<H> {
34 pub fn new(http: H, network: Network, clock: Clock) -> Self {
35 Api { http, network, clock, limiter: Mutex::new(Limiter::default()) }
36 }
37
38 #[must_use]
39 pub const fn network(&self) -> Network {
40 self.network
41 }
42
43 #[must_use]
44 pub const fn http(&self) -> &H {
45 &self.http
46 }
47
48 pub fn spent(&self) -> Spent {
50 let now = (self.clock)();
51 let mut limiter = self.limiter();
52 let cooldown_ms = limiter.gate.cooldown_ms(now);
53 Spent { cooldown_ms, ..limiter.meter.spent(now) }
54 }
55
56 fn limiter(&self) -> MutexGuard<'_, Limiter> {
57 self.limiter.lock().unwrap_or_else(PoisonError::into_inner)
58 }
59}
60
61impl<H: Http> Transport for Api<H> {
62 async fn post(&self, path: &str, body: String) -> Result<String, TransportError> {
66 let is_read = path == INFO;
67 let kind = match is_read {
68 true => Json::parse(&body)
69 .ok()
70 .and_then(|request| request.get("type").and_then(Json::as_str).map(ToOwned::to_owned))
71 .unwrap_or_default(),
72 false => EXCHANGE_KIND.to_owned(),
73 };
74 let batched = match is_read {
75 true => 0,
76 false => batched(&body),
77 };
78 let ticket = {
79 let now = (self.clock)();
80 let mut limiter = self.limiter();
81 let ticket = limiter.gate.open(now).map_err(|wait| rate_limited(Some(wait)))?;
82 if is_read && let Some(wait) = limiter.meter.wait_for(now, base_weight(&kind)) {
83 return Err(rate_limited(Some(wait)));
84 }
85 ticket
86 };
87 let request = Request::post_json(format!("{}{path}", self.network.api_url()), body);
88 let answer = self.http.send(request).await;
89 let now = (self.clock)();
90 let mut limiter = self.limiter();
91 let response = answer?;
92 if response.status == 429 {
93 limiter.meter.refuse(now);
94 let retry_after_ms = response.header("retry-after").and_then(|seconds| seconds.trim().parse::<u64>().ok());
95 let wait = limiter.gate.struck(ticket, now, retry_after_ms.map(|seconds| seconds.saturating_mul(1_000)));
96 return Err(rate_limited(Some(wait)));
97 }
98 let text = response.text();
99 let weight = match (is_read, response.is_success()) {
100 (true, true) => read_weight(&kind, per_items(&kind).map_or(0, |_| items(&text))),
101 (true, false) => base_weight(&kind),
102 (false, _) => action_weight(batched),
103 };
104 limiter.meter.record(now, &kind, weight);
105 match response.is_success() {
106 true => {
107 limiter.gate.passed(ticket);
108 Ok(text)
109 }
110 false => Err(TransportError::Status { status: response.status, body: text }),
111 }
112 }
113}
114
115const fn rate_limited(retry_after_ms: Option<u64>) -> TransportError {
116 TransportError::RateLimited { retry_after_ms }
117}
118
119fn batched(body: &str) -> usize {
121 let Ok(body) = Json::parse(body) else { return 0 };
122 let Some(action) = body.get("action") else { return 0 };
123 ["orders", "cancels", "modifies"]
124 .iter()
125 .find_map(|key| action.get(key).and_then(Json::as_array))
126 .map_or(0, <[Json]>::len)
127}
128
129fn items(text: &str) -> usize {
131 serde_json::from_str::<Vec<serde::de::IgnoredAny>>(text).map_or(0, |list| list.len())
132}
133
134#[cfg(test)]
135#[path = "tests/api.rs"]
136mod tests;