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