Skip to main content

df_hyperliquid_core/client/
mod.rs

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