Skip to main content

df_hyperliquid_core/client/native/
mod.rs

1//! HTTP and websocket for native programs: [`Native`] sends requests through
2//! reqwest on rustls, [`clock`] is the system clock, and a [`Feed`] keeps the
3//! websocket open over tokio-tungstenite. Hand the first two to a
4//! [`crate::Client`]:
5//!
6//! ```no_run
7//! use df_hyperliquid_core::client::native::{Native, clock};
8//! use df_hyperliquid_core::{Client, Network};
9//!
10//! let client = Client::reader(Native::new(), Network::Mainnet, clock);
11//! ```
12//!
13//! A [`Native`] future must be polled inside a tokio runtime. Nothing here
14//! retries. [`keep_warm`] holds a client's connection open between orders, so
15//! the first one after a quiet spell skips DNS and the TLS handshake.
16
17pub mod socket;
18
19use crate::client::{Client, Http, Method, Request, Response, TransportError};
20use std::sync::Arc;
21use std::time::{Duration, SystemTime, UNIX_EPOCH};
22
23pub use socket::Feed;
24
25/// How long a request may take, connection and answer together, unless the
26/// program says otherwise.
27pub const TIMEOUT: Duration = Duration::from_secs(30);
28
29/// One HTTP client, cheap to clone: clones share their connections.
30#[derive(Debug, Clone)]
31pub struct Native {
32    client: reqwest::Client,
33}
34
35impl Native {
36    /// A client with [`TIMEOUT`].
37    #[must_use]
38    pub fn new() -> Self {
39        Native::with_timeout(TIMEOUT)
40    }
41
42    /// A client whose every request gives up after `timeout`. A request that
43    /// times out comes back as [`TransportError::Unreachable`]: an action may
44    /// still have landed.
45    #[must_use]
46    pub fn with_timeout(timeout: Duration) -> Self {
47        let client = reqwest::Client::builder().timeout(timeout).build().unwrap_or_else(|_| reqwest::Client::new());
48        Native { client }
49    }
50
51    /// Requests through a client the program built itself: its own user agent, pool or timeouts.
52    #[must_use]
53    pub const fn with_client(client: reqwest::Client) -> Self {
54        Native { client }
55    }
56}
57
58impl Default for Native {
59    fn default() -> Self {
60        Native::new()
61    }
62}
63
64impl Http for Native {
65    async fn send(&self, request: Request) -> Result<Response, TransportError> {
66        request.refuse_if_offline()?;
67        let unreachable = |error: reqwest::Error| TransportError::Unreachable(error.without_url().to_string());
68        let mut outgoing = self.client.request(method(request.method), &request.url);
69        for (name, value) in &request.headers {
70            outgoing = outgoing.header(name, value);
71        }
72        if !request.body.is_empty() {
73            outgoing = outgoing.body(request.body);
74        }
75        let answer = outgoing.send().await.map_err(unreachable)?;
76        let status = answer.status().as_u16();
77        let headers = answer
78            .headers()
79            .iter()
80            .map(|(name, value)| (name.as_str().to_owned(), String::from_utf8_lossy(value.as_bytes()).into_owned()))
81            .collect();
82        let body = answer.bytes().await.map_err(unreachable)?.to_vec();
83        Ok(Response { status, headers, body })
84    }
85}
86
87fn method(method: Method) -> reqwest::Method {
88    match method {
89        Method::Get => reqwest::Method::GET,
90        Method::Post => reqwest::Method::POST,
91    }
92}
93
94/// How often [`keep_warm`] reads, well inside the time a pooled connection is kept idle.
95pub const KEEP_WARM: Duration = Duration::from_secs(30);
96
97/// Warms `client` every `every` while anything else holds it, and ends by itself once the last
98/// holder lets go. Each warm is [`Client::warm`]: one of the lightest reads, never signed.
99pub fn keep_warm<S: Send + Sync + 'static>(
100    client: &Arc<Client<Native, S>>,
101    every: Duration,
102) -> tokio::task::JoinHandle<()> {
103    let held = Arc::downgrade(client);
104    tokio::spawn(async move {
105        let mut ticks = tokio::time::interval(every);
106        loop {
107            ticks.tick().await;
108            let Some(client) = held.upgrade() else {
109                return;
110            };
111            client.warm().await;
112        }
113    })
114}
115
116/// Unix milliseconds from the system clock: the [`crate::client::Clock`] for a
117/// native program. A clock set before 1970 reads 0.
118#[must_use]
119pub fn clock() -> u64 {
120    SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |since| u64::try_from(since.as_millis()).unwrap_or(u64::MAX))
121}
122
123#[cfg(test)]
124#[path = "tests/http.rs"]
125mod tests;