Skip to main content

hyper_exchange/
client.rs

1use crate::types::{ExchangeError, Signature};
2
3/// Mainnet REST API URL.
4const MAINNET_API_URL: &str = "https://api.hyperliquid.xyz";
5/// Testnet REST API URL.
6const TESTNET_API_URL: &str = "https://api.hyperliquid-testnet.xyz";
7
8/// Hyperliquid Exchange REST client.
9///
10/// Handles sending signed actions to the exchange API and querying
11/// the info endpoint.
12pub struct ExchangeClient {
13    http: reqwest::Client,
14    base_url: String,
15    is_mainnet: bool,
16}
17
18impl ExchangeClient {
19    /// Create a new exchange client for mainnet or testnet.
20    pub fn new(is_mainnet: bool) -> Self {
21        let base_url = Self::base_url_for(is_mainnet).to_string();
22        Self {
23            http: reqwest::Client::new(),
24            base_url,
25            is_mainnet,
26        }
27    }
28
29    /// Returns the base URL for the given network.
30    pub fn base_url_for(is_mainnet: bool) -> &'static str {
31        if is_mainnet {
32            MAINNET_API_URL
33        } else {
34            TESTNET_API_URL
35        }
36    }
37
38    /// Whether this client targets mainnet.
39    pub fn is_mainnet(&self) -> bool {
40        self.is_mainnet
41    }
42
43    /// Send a signed action to the exchange `/exchange` endpoint.
44    ///
45    /// The payload includes the action, signature, nonce, and optional vault address.
46    pub async fn post_action(
47        &self,
48        action: serde_json::Value,
49        signature: &Signature,
50        nonce: u64,
51        vault_address: Option<&str>,
52    ) -> Result<serde_json::Value, ExchangeError> {
53        let mut payload = serde_json::json!({
54            "action": action,
55            "nonce": nonce,
56            "signature": {
57                "r": signature.r,
58                "s": signature.s,
59                "v": signature.v,
60            },
61        });
62
63        if let Some(vault) = vault_address {
64            payload.as_object_mut().unwrap().insert(
65                "vaultAddress".to_string(),
66                serde_json::Value::String(vault.to_string()),
67            );
68        }
69
70        let url = format!("{}/exchange", self.base_url);
71        let response = self
72            .http
73            .post(&url)
74            .json(&payload)
75            .send()
76            .await
77            .map_err(|e| ExchangeError::HttpError(e.to_string()))?;
78
79        let status = response.status();
80        let body: serde_json::Value = response
81            .json()
82            .await
83            .map_err(|e| ExchangeError::HttpError(format!("Failed to parse response: {}", e)))?;
84
85        if !status.is_success() {
86            return Err(ExchangeError::ApiError(format!(
87                "HTTP {}: {}",
88                status, body
89            )));
90        }
91
92        Ok(body)
93    }
94
95    /// Query the info API at `/info`.
96    ///
97    /// Used for read-only queries like clearinghouseState, meta, l2Book, etc.
98    pub async fn post_info(
99        &self,
100        request: serde_json::Value,
101    ) -> Result<serde_json::Value, ExchangeError> {
102        let url = format!("{}/info", self.base_url);
103        let response = self
104            .http
105            .post(&url)
106            .json(&request)
107            .send()
108            .await
109            .map_err(|e| ExchangeError::HttpError(e.to_string()))?;
110
111        let status = response.status();
112        let body: serde_json::Value = response
113            .json()
114            .await
115            .map_err(|e| ExchangeError::HttpError(format!("Failed to parse response: {}", e)))?;
116
117        if !status.is_success() {
118            return Err(ExchangeError::ApiError(format!(
119                "HTTP {}: {}",
120                status, body
121            )));
122        }
123
124        Ok(body)
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_base_url_mainnet() {
134        assert_eq!(ExchangeClient::base_url_for(true), MAINNET_API_URL);
135    }
136
137    #[test]
138    fn test_base_url_testnet() {
139        assert_eq!(ExchangeClient::base_url_for(false), TESTNET_API_URL);
140    }
141
142    #[test]
143    fn test_new_client_mainnet() {
144        let client = ExchangeClient::new(true);
145        assert!(client.is_mainnet());
146        assert_eq!(client.base_url, MAINNET_API_URL);
147    }
148
149    #[test]
150    fn test_new_client_testnet() {
151        let client = ExchangeClient::new(false);
152        assert!(!client.is_mainnet());
153        assert_eq!(client.base_url, TESTNET_API_URL);
154    }
155}