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.
15
16pub mod socket;
17
18use crate::client::{Http, Method, Request, Response, TransportError};
19use std::time::{Duration, SystemTime, UNIX_EPOCH};
20
21pub use socket::Feed;
22
23/// How long a request may take, connection and answer together, unless the
24/// program says otherwise.
25pub const TIMEOUT: Duration = Duration::from_secs(30);
26
27/// One HTTP client, cheap to clone: clones share their connections.
28#[derive(Debug, Clone)]
29pub struct Native {
30    client: reqwest::Client,
31}
32
33impl Native {
34    /// A client with [`TIMEOUT`].
35    #[must_use]
36    pub fn new() -> Self {
37        Native::with_timeout(TIMEOUT)
38    }
39
40    /// A client whose every request gives up after `timeout`. A request that
41    /// times out comes back as [`TransportError::Unreachable`]: an action may
42    /// still have landed.
43    #[must_use]
44    pub fn with_timeout(timeout: Duration) -> Self {
45        let client = reqwest::Client::builder().timeout(timeout).build().unwrap_or_else(|_| reqwest::Client::new());
46        Native { client }
47    }
48}
49
50impl Default for Native {
51    fn default() -> Self {
52        Native::new()
53    }
54}
55
56impl Http for Native {
57    async fn send(&self, request: Request) -> Result<Response, TransportError> {
58        let unreachable = |error: reqwest::Error| TransportError::Unreachable(error.without_url().to_string());
59        let mut outgoing = self.client.request(method(request.method), &request.url);
60        for (name, value) in &request.headers {
61            outgoing = outgoing.header(name, value);
62        }
63        if !request.body.is_empty() {
64            outgoing = outgoing.body(request.body);
65        }
66        let answer = outgoing.send().await.map_err(unreachable)?;
67        let status = answer.status().as_u16();
68        let headers = answer
69            .headers()
70            .iter()
71            .map(|(name, value)| (name.as_str().to_owned(), String::from_utf8_lossy(value.as_bytes()).into_owned()))
72            .collect();
73        let body = answer.bytes().await.map_err(unreachable)?.to_vec();
74        Ok(Response { status, headers, body })
75    }
76}
77
78fn method(method: Method) -> reqwest::Method {
79    match method {
80        Method::Get => reqwest::Method::GET,
81        Method::Post => reqwest::Method::POST,
82    }
83}
84
85/// Unix milliseconds from the system clock: the [`crate::client::Clock`] for a
86/// native program. A clock set before 1970 reads 0.
87#[must_use]
88pub fn clock() -> u64 {
89    SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |since| u64::try_from(since.as_millis()).unwrap_or(u64::MAX))
90}
91
92#[cfg(test)]
93#[path = "tests/http.rs"]
94mod tests;