ccxt_exchanges/hyperliquid/mod.rs
1//! HyperLiquid exchange implementation.
2//!
3//! HyperLiquid is a decentralized perpetual futures exchange built on its own L1 blockchain.
4//! Unlike centralized exchanges (CEX) like Binance or Bitget, HyperLiquid uses:
5//! - Ethereum wallet private keys for authentication (EIP-712 typed data signatures)
6//! - Wallet addresses as account identifiers (no registration required)
7//! - USDC as the sole settlement currency
8//!
9//! # Features
10//!
11//! - Perpetual futures trading with up to 50x leverage
12//! - Cross-margin and isolated margin modes
13//! - Real-time WebSocket data streaming
14//! - EIP-712 compliant transaction signing
15//!
16//! # Note on Market Types
17//!
18//! HyperLiquid only supports perpetual futures (Swap). Attempting to configure
19//! other market types (Spot, Futures, Margin, Option) will result in an error.
20//!
21//! # Example
22//!
23//! ```no_run
24//! use ccxt_exchanges::hyperliquid::HyperLiquid;
25//!
26//! # async fn example() -> ccxt_core::Result<()> {
27//! // Create a public-only instance (no authentication)
28//! let exchange = HyperLiquid::builder()
29//! .testnet(true)
30//! .build()?;
31//!
32//! // Fetch markets
33//! let markets = exchange.fetch_markets().await?;
34//! println!("Found {} markets", markets.len());
35//!
36//! // Create an authenticated instance
37//! let exchange = HyperLiquid::builder()
38//! .private_key("0x...")
39//! .testnet(true)
40//! .build()?;
41//!
42//! // Fetch balance
43//! let balance = exchange.fetch_balance().await?;
44//! # Ok(())
45//! # }
46//! ```
47
48use ccxt_core::types::default_type::DefaultType;
49use ccxt_core::{BaseExchange, ExchangeConfig, Result};
50
51pub mod auth;
52pub mod builder;
53pub mod endpoint_router;
54pub mod error;
55mod exchange_impl;
56pub mod parser;
57pub mod rest;
58pub mod signed_request;
59pub mod ws;
60mod ws_exchange_impl;
61
62pub use auth::HyperLiquidAuth;
63pub use builder::{HyperLiquidBuilder, validate_default_type};
64pub use endpoint_router::HyperLiquidEndpointRouter;
65pub use error::{HyperLiquidErrorCode, is_error_response, parse_error};
66
67/// HyperLiquid exchange structure.
68#[derive(Debug)]
69pub struct HyperLiquid {
70 /// Base exchange instance.
71 base: BaseExchange,
72 /// HyperLiquid-specific options.
73 options: HyperLiquidOptions,
74 /// Authentication instance (optional, for private API).
75 auth: Option<HyperLiquidAuth>,
76 /// Persistent WebSocket connection reference.
77 pub(crate) ws_connection: std::sync::Arc<tokio::sync::RwLock<Option<ws::HyperLiquidWs>>>,
78}
79
80/// HyperLiquid-specific options.
81///
82/// Note: HyperLiquid only supports perpetual futures (Swap). The `default_type`
83/// field defaults to `Swap` and attempting to set it to any other value will
84/// result in a validation error.
85#[derive(Debug, Clone)]
86pub struct HyperLiquidOptions {
87 /// Whether to use testnet.
88 pub testnet: bool,
89 /// Vault address for vault trading (optional).
90 pub vault_address: Option<String>,
91 /// Default leverage multiplier.
92 pub default_leverage: u32,
93 /// Default market type for trading.
94 ///
95 /// HyperLiquid only supports perpetual futures, so this must be `Swap`.
96 /// Attempting to set any other value will result in a validation error.
97 pub default_type: DefaultType,
98}
99
100impl Default for HyperLiquidOptions {
101 fn default() -> Self {
102 Self {
103 testnet: false,
104 vault_address: None,
105 default_leverage: 1,
106 // HyperLiquid only supports perpetual futures (Swap)
107 default_type: DefaultType::Swap,
108 }
109 }
110}
111
112impl HyperLiquid {
113 /// Creates a new HyperLiquid instance using the builder pattern.
114 ///
115 /// This is the recommended way to create a HyperLiquid instance.
116 ///
117 /// # Example
118 ///
119 /// ```no_run
120 /// use ccxt_exchanges::hyperliquid::HyperLiquid;
121 ///
122 /// let exchange = HyperLiquid::builder()
123 /// .private_key("0x...")
124 /// .testnet(true)
125 /// .build()
126 /// .unwrap();
127 /// ```
128 pub fn builder() -> HyperLiquidBuilder {
129 HyperLiquidBuilder::new()
130 }
131
132 /// Creates a new HyperLiquid instance with custom options.
133 ///
134 /// This is used internally by the builder pattern.
135 ///
136 /// # Arguments
137 ///
138 /// * `config` - Exchange configuration.
139 /// * `options` - HyperLiquid-specific options.
140 /// * `auth` - Optional authentication instance.
141 pub fn new_with_options(
142 config: ExchangeConfig,
143 options: HyperLiquidOptions,
144 auth: Option<HyperLiquidAuth>,
145 ) -> Result<Self> {
146 let base = BaseExchange::new(config)?;
147 Ok(Self {
148 base,
149 options,
150 auth,
151 ws_connection: std::sync::Arc::new(tokio::sync::RwLock::new(None)),
152 })
153 }
154
155 /// Returns a reference to the base exchange.
156 pub fn base(&self) -> &BaseExchange {
157 &self.base
158 }
159
160 /// Returns a mutable reference to the base exchange.
161 pub fn base_mut(&mut self) -> &mut BaseExchange {
162 &mut self.base
163 }
164
165 /// Returns the HyperLiquid options.
166 pub fn options(&self) -> &HyperLiquidOptions {
167 &self.options
168 }
169
170 /// Returns a reference to the authentication instance.
171 pub fn auth(&self) -> Option<&HyperLiquidAuth> {
172 self.auth.as_ref()
173 }
174
175 /// Creates a signed action builder for authenticated exchange requests.
176 ///
177 /// This method provides a fluent API for constructing and executing
178 /// authenticated Hyperliquid exchange actions using EIP-712 signing.
179 ///
180 /// # Arguments
181 ///
182 /// * `action` - The action JSON to be signed and executed
183 ///
184 /// # Returns
185 ///
186 /// A `HyperliquidSignedRequestBuilder` that can be configured and executed.
187 ///
188 /// # Example
189 ///
190 /// ```no_run
191 /// use ccxt_exchanges::hyperliquid::HyperLiquid;
192 /// use serde_json::json;
193 ///
194 /// # async fn example() -> ccxt_core::Result<()> {
195 /// let exchange = HyperLiquid::builder()
196 /// .private_key("0x...")
197 /// .testnet(true)
198 /// .build()?;
199 ///
200 /// // Create an order
201 /// let action = json!({
202 /// "type": "order",
203 /// "orders": [{"a": 0, "b": true, "p": "50000", "s": "0.001", "r": false, "t": {"limit": {"tif": "Gtc"}}}],
204 /// "grouping": "na"
205 /// });
206 ///
207 /// let response = exchange.signed_action(action)
208 /// .execute()
209 /// .await?;
210 /// # Ok(())
211 /// # }
212 /// ```
213 pub fn signed_action(
214 &self,
215 action: serde_json::Value,
216 ) -> signed_request::HyperliquidSignedRequestBuilder<'_> {
217 signed_request::HyperliquidSignedRequestBuilder::new(self, action)
218 }
219
220 /// Returns the exchange ID.
221 pub fn id(&self) -> &'static str {
222 "hyperliquid"
223 }
224
225 /// Returns the exchange name.
226 pub fn name(&self) -> &'static str {
227 "HyperLiquid"
228 }
229
230 /// Returns the API version.
231 pub fn version(&self) -> &'static str {
232 "1"
233 }
234
235 /// Returns `true` if the exchange is CCXT-certified.
236 pub fn certified(&self) -> bool {
237 false
238 }
239
240 /// Returns `true` if Pro version (WebSocket) is supported.
241 pub fn pro(&self) -> bool {
242 true
243 }
244
245 /// Returns the rate limit in requests per second.
246 pub fn rate_limit(&self) -> u32 {
247 // HyperLiquid has generous rate limits
248 100
249 }
250
251 /// Returns `true` if sandbox/testnet mode is enabled.
252 ///
253 /// Sandbox mode is enabled when either:
254 /// - `config.sandbox` is set to `true`
255 /// - `options.testnet` is set to `true`
256 ///
257 /// # Returns
258 ///
259 /// `true` if sandbox mode is enabled, `false` otherwise.
260 ///
261 /// # Example
262 ///
263 /// ```no_run
264 /// use ccxt_exchanges::hyperliquid::HyperLiquid;
265 ///
266 /// let exchange = HyperLiquid::builder()
267 /// .testnet(true)
268 /// .build()
269 /// .unwrap();
270 /// assert!(exchange.is_sandbox());
271 /// ```
272 pub fn is_sandbox(&self) -> bool {
273 self.base().config.sandbox || self.options.testnet
274 }
275
276 /// Returns the API URLs.
277 ///
278 /// Returns testnet URLs when sandbox mode is enabled (either via
279 /// `config.sandbox` or `options.testnet`), otherwise returns mainnet URLs.
280 pub fn urls(&self) -> HyperLiquidUrls {
281 if self.is_sandbox() {
282 HyperLiquidUrls::testnet()
283 } else {
284 HyperLiquidUrls::mainnet()
285 }
286 }
287
288 /// Returns the wallet address if authenticated.
289 pub fn wallet_address(&self) -> Option<&str> {
290 self.auth
291 .as_ref()
292 .map(auth::HyperLiquidAuth::wallet_address)
293 }
294
295 /// Creates a WebSocket client for public data streams.
296 pub fn create_ws(&self) -> ws::HyperLiquidWs {
297 let urls = self.urls();
298 ws::HyperLiquidWs::new(urls.ws)
299 }
300}
301
302/// HyperLiquid API URLs.
303#[derive(Debug, Clone)]
304pub struct HyperLiquidUrls {
305 /// REST API base URL.
306 pub rest: String,
307 /// WebSocket URL.
308 pub ws: String,
309}
310
311impl HyperLiquidUrls {
312 /// Returns mainnet environment URLs.
313 pub fn mainnet() -> Self {
314 Self {
315 rest: "https://api.hyperliquid.xyz".to_string(),
316 ws: "wss://api.hyperliquid.xyz/ws".to_string(),
317 }
318 }
319
320 /// Returns testnet environment URLs.
321 pub fn testnet() -> Self {
322 Self {
323 rest: "https://api.hyperliquid-testnet.xyz".to_string(),
324 ws: "wss://api.hyperliquid-testnet.xyz/ws".to_string(),
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_default_options() {
335 let options = HyperLiquidOptions::default();
336 assert!(!options.testnet);
337 assert!(options.vault_address.is_none());
338 assert_eq!(options.default_leverage, 1);
339 // HyperLiquid only supports perpetual futures, so default_type must be Swap
340 assert_eq!(options.default_type, DefaultType::Swap);
341 }
342
343 #[test]
344 fn test_mainnet_urls() {
345 let urls = HyperLiquidUrls::mainnet();
346 assert_eq!(urls.rest, "https://api.hyperliquid.xyz");
347 assert_eq!(urls.ws, "wss://api.hyperliquid.xyz/ws");
348 }
349
350 #[test]
351 fn test_testnet_urls() {
352 let urls = HyperLiquidUrls::testnet();
353 assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
354 assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
355 }
356
357 #[test]
358 fn test_is_sandbox_with_options_testnet() {
359 let config = ExchangeConfig::default();
360 let options = HyperLiquidOptions {
361 testnet: true,
362 ..Default::default()
363 };
364 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
365 assert!(exchange.is_sandbox());
366 }
367
368 #[test]
369 fn test_is_sandbox_with_config_sandbox() {
370 let config = ExchangeConfig {
371 sandbox: true,
372 ..Default::default()
373 };
374 let options = HyperLiquidOptions::default();
375 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
376 assert!(exchange.is_sandbox());
377 }
378
379 #[test]
380 fn test_is_sandbox_false_by_default() {
381 let config = ExchangeConfig::default();
382 let options = HyperLiquidOptions::default();
383 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
384 assert!(!exchange.is_sandbox());
385 }
386
387 #[test]
388 fn test_urls_returns_mainnet_by_default() {
389 let config = ExchangeConfig::default();
390 let options = HyperLiquidOptions::default();
391 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
392 let urls = exchange.urls();
393 assert_eq!(urls.rest, "https://api.hyperliquid.xyz");
394 assert_eq!(urls.ws, "wss://api.hyperliquid.xyz/ws");
395 }
396
397 #[test]
398 fn test_urls_returns_testnet_with_options_testnet() {
399 let config = ExchangeConfig::default();
400 let options = HyperLiquidOptions {
401 testnet: true,
402 ..Default::default()
403 };
404 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
405 let urls = exchange.urls();
406 assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
407 assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
408 }
409
410 #[test]
411 fn test_urls_returns_testnet_with_config_sandbox() {
412 let config = ExchangeConfig {
413 sandbox: true,
414 ..Default::default()
415 };
416 let options = HyperLiquidOptions::default();
417 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
418 let urls = exchange.urls();
419 assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
420 assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
421 }
422}