Skip to main content

df_hyperliquid_core/client/
mod.rs

1//! Connecting to Hyperliquid: the [`Client`] that reads, signs and sends over one network's
2//! rate-limited `/info` and `/exchange` ([`http`]), the networks, HyperEVM ([`evm`]), and the
3//! native and browser wires and websockets.
4
5pub mod evm;
6pub mod http;
7#[cfg(feature = "native")]
8pub mod native;
9pub mod network;
10#[cfg(feature = "web")]
11pub mod web;
12
13use crate::account::SignError;
14#[cfg(feature = "sign")]
15use crate::account::Signer;
16use crate::client::http::Api;
17use crate::client::http::INFO;
18#[cfg(feature = "sign")]
19use crate::utils::sign::{Domain, L1Action, UserAction};
20use crate::utils::{Address, NonceClock};
21use std::sync::{Mutex, PoisonError};
22
23pub use http::{
24    Accepted, ApiError, ExchangeResponse, Http, Method, OFFLINE, Request, Response, Spent, Status, Transport,
25    TransportError, exchange, info, info_request, user_request,
26};
27pub use network::{
28    L1_CHAIN_ID, MAINNET_HYPE, MAINNET_USDC, Network, TESTNET_HYPE, TESTNET_USDC, Token, USDC_INDEX, UnknownNetwork,
29};
30
31/// Unix milliseconds. Injected: std has no clock in the browser or Workers,
32/// and a test pins the time.
33pub type Clock = fn() -> u64;
34
35/// Why an action was not taken. An action whose answer never came
36/// ([`TransportError::Unreachable`]) may have landed, so it is never resent.
37#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
38pub enum SendError {
39    #[error(transparent)]
40    Sign(#[from] SignError),
41    #[error(transparent)]
42    Api(#[from] ApiError),
43}
44
45/// Reads for any module (`markets::perps::meta(&client)`) and, with a
46/// [`Signer`], actions. `S` is `()` for a client that only reads.
47pub struct Client<H, S = ()> {
48    api: Api<H>,
49    signer: S,
50    vault: Option<Address>,
51    clock: Clock,
52    nonces: Mutex<NonceClock>,
53}
54
55/// Its network and the vault it acts for; never its signer or its transport.
56impl<H: Http, S> std::fmt::Debug for Client<H, S> {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("Client").field("network", &self.network()).field("vault", &self.vault).finish_non_exhaustive()
59    }
60}
61
62impl<H: Http> Client<H> {
63    /// A client that reads and never signs.
64    pub fn reader(http: H, network: Network, clock: Clock) -> Self {
65        Client::new(http, network, clock, ())
66    }
67}
68
69impl<H: Http, S> Client<H, S> {
70    /// One network, through `http`, signing with `signer`: the account's own
71    /// key or wallet, or an agent for the actions an agent may sign.
72    pub fn new(http: H, network: Network, clock: Clock, signer: S) -> Self {
73        Client {
74            api: Api::new(http, network, clock),
75            signer,
76            vault: None,
77            clock,
78            nonces: Mutex::new(NonceClock::new()),
79        }
80    }
81
82    /// Acts for a subaccount or vault: its L1 actions name it as
83    /// `vaultAddress`, and are still signed by this client's signer.
84    #[must_use]
85    pub fn acting_for(mut self, vault: Address) -> Self {
86        self.vault = Some(vault);
87        self
88    }
89
90    #[must_use]
91    pub const fn network(&self) -> Network {
92        self.api.network()
93    }
94
95    #[must_use]
96    pub const fn http(&self) -> &H {
97        self.api.http()
98    }
99
100    #[must_use]
101    pub const fn signer(&self) -> &S {
102        &self.signer
103    }
104
105    /// What went out over the last minute, and whether the gate is closed.
106    pub fn spent(&self) -> Spent {
107        self.api.spent()
108    }
109
110    /// Opens, or refreshes, the connection actions travel on, with one of the venue's lightest
111    /// reads (`exchangeStatus`, weight 2), counted like any other. Nothing is signed and the
112    /// answer is dropped: a program warms before its first order so that order skips DNS and
113    /// the TLS handshake, and [`native::keep_warm`] repeats it between orders.
114    pub async fn warm(&self) {
115        let _ = self.api.post(INFO, info_request("exchangeStatus", &[])).await;
116    }
117
118    /// The next nonce: the clock, or one past the last if the clock has not
119    /// moved. Each client keeps its own, so give every signer one client.
120    pub fn next_nonce(&self) -> u64 {
121        self.nonces.lock().unwrap_or_else(PoisonError::into_inner).next((self.clock)())
122    }
123}
124
125/// Sending needs the `sign` feature: each signature is recovered and must
126/// match the signer before anything is posted, since a wallet can sign as a
127/// different account than expected.
128#[cfg(feature = "sign")]
129impl<H: Http, S: Signer> Client<H, S> {
130    /// Signs an L1 action at the next nonce and sends it once.
131    pub async fn send(&self, action: &impl L1Action) -> Result<Accepted, SendError> {
132        self.send_at(action, self.next_nonce()).await
133    }
134
135    /// Signs an L1 action at `nonce` and sends it once. The nonce must be new
136    /// for this signer; [`Client::send`] takes care of that.
137    pub async fn send_at(&self, action: &impl L1Action, nonce: u64) -> Result<Accepted, SendError> {
138        let typed = action.typed(nonce, self.vault, self.network());
139        let signature = self.signer.sign_typed(&Domain::EXCHANGE, &typed).await?;
140        self.check(&typed.digest(&Domain::EXCHANGE), &signature)?;
141        Ok(exchange(&self.api, action.body(nonce, &signature, self.vault)).await?)
142    }
143
144    /// Signs a user-signed action under this network's chain and sends it
145    /// once. Its nonce is the one it was built with ([`Client::next_nonce`]
146    /// gives one). Only the account's own key may sign these.
147    pub async fn send_user(&self, action: &impl UserAction) -> Result<Accepted, SendError> {
148        let network = self.network();
149        let domain = Domain::user_signed(network);
150        let typed = action.typed(network);
151        let signature = self.signer.sign_typed(&domain, &typed).await?;
152        self.check(&typed.digest(&domain), &signature)?;
153        Ok(exchange(&self.api, action.body(&signature, network)).await?)
154    }
155
156    /// The signature must recover to the signer before it is posted.
157    fn check(&self, digest: &[u8; 32], signature: &crate::account::Signature) -> Result<(), SignError> {
158        crate::account::verify(digest, signature, self.signer.address())
159    }
160}
161
162impl<H: Http, S> Transport for Client<H, S> {
163    fn post(&self, path: &str, body: String) -> impl Future<Output = Result<String, TransportError>> {
164        self.api.post(path, body)
165    }
166}