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}
77
78/// HyperLiquid-specific options.
79///
80/// Note: HyperLiquid only supports perpetual futures (Swap). The `default_type`
81/// field defaults to `Swap` and attempting to set it to any other value will
82/// result in a validation error.
83#[derive(Debug, Clone)]
84pub struct HyperLiquidOptions {
85 /// Whether to use testnet.
86 pub testnet: bool,
87 /// Vault address for vault trading (optional).
88 pub vault_address: Option<String>,
89 /// Default leverage multiplier.
90 pub default_leverage: u32,
91 /// Default market type for trading.
92 ///
93 /// HyperLiquid only supports perpetual futures, so this must be `Swap`.
94 /// Attempting to set any other value will result in a validation error.
95 pub default_type: DefaultType,
96}
97
98impl Default for HyperLiquidOptions {
99 fn default() -> Self {
100 Self {
101 testnet: false,
102 vault_address: None,
103 default_leverage: 1,
104 // HyperLiquid only supports perpetual futures (Swap)
105 default_type: DefaultType::Swap,
106 }
107 }
108}
109
110impl HyperLiquid {
111 /// Creates a new HyperLiquid instance using the builder pattern.
112 ///
113 /// This is the recommended way to create a HyperLiquid instance.
114 ///
115 /// # Example
116 ///
117 /// ```no_run
118 /// use ccxt_exchanges::hyperliquid::HyperLiquid;
119 ///
120 /// let exchange = HyperLiquid::builder()
121 /// .private_key("0x...")
122 /// .testnet(true)
123 /// .build()
124 /// .unwrap();
125 /// ```
126 pub fn builder() -> HyperLiquidBuilder {
127 HyperLiquidBuilder::new()
128 }
129
130 /// Creates a new HyperLiquid instance with custom options.
131 ///
132 /// This is used internally by the builder pattern.
133 ///
134 /// # Arguments
135 ///
136 /// * `config` - Exchange configuration.
137 /// * `options` - HyperLiquid-specific options.
138 /// * `auth` - Optional authentication instance.
139 pub fn new_with_options(
140 config: ExchangeConfig,
141 options: HyperLiquidOptions,
142 auth: Option<HyperLiquidAuth>,
143 ) -> Result<Self> {
144 let base = BaseExchange::new(config)?;
145 Ok(Self {
146 base,
147 options,
148 auth,
149 })
150 }
151
152 /// Returns a reference to the base exchange.
153 pub fn base(&self) -> &BaseExchange {
154 &self.base
155 }
156
157 /// Returns a mutable reference to the base exchange.
158 pub fn base_mut(&mut self) -> &mut BaseExchange {
159 &mut self.base
160 }
161
162 /// Returns the HyperLiquid options.
163 pub fn options(&self) -> &HyperLiquidOptions {
164 &self.options
165 }
166
167 /// Returns a reference to the authentication instance.
168 pub fn auth(&self) -> Option<&HyperLiquidAuth> {
169 self.auth.as_ref()
170 }
171
172 /// Creates a signed action builder for authenticated exchange requests.
173 ///
174 /// This method provides a fluent API for constructing and executing
175 /// authenticated Hyperliquid exchange actions using EIP-712 signing.
176 ///
177 /// # Arguments
178 ///
179 /// * `action` - The action JSON to be signed and executed
180 ///
181 /// # Returns
182 ///
183 /// A `HyperliquidSignedRequestBuilder` that can be configured and executed.
184 ///
185 /// # Example
186 ///
187 /// ```no_run
188 /// use ccxt_exchanges::hyperliquid::HyperLiquid;
189 /// use serde_json::json;
190 ///
191 /// # async fn example() -> ccxt_core::Result<()> {
192 /// let exchange = HyperLiquid::builder()
193 /// .private_key("0x...")
194 /// .testnet(true)
195 /// .build()?;
196 ///
197 /// // Create an order
198 /// let action = json!({
199 /// "type": "order",
200 /// "orders": [{"a": 0, "b": true, "p": "50000", "s": "0.001", "r": false, "t": {"limit": {"tif": "Gtc"}}}],
201 /// "grouping": "na"
202 /// });
203 ///
204 /// let response = exchange.signed_action(action)
205 /// .execute()
206 /// .await?;
207 /// # Ok(())
208 /// # }
209 /// ```
210 pub fn signed_action(
211 &self,
212 action: serde_json::Value,
213 ) -> signed_request::HyperliquidSignedRequestBuilder<'_> {
214 signed_request::HyperliquidSignedRequestBuilder::new(self, action)
215 }
216
217 /// Returns the exchange ID.
218 pub fn id(&self) -> &'static str {
219 "hyperliquid"
220 }
221
222 /// Returns the exchange name.
223 pub fn name(&self) -> &'static str {
224 "HyperLiquid"
225 }
226
227 /// Returns the API version.
228 pub fn version(&self) -> &'static str {
229 "1"
230 }
231
232 /// Returns `true` if the exchange is CCXT-certified.
233 pub fn certified(&self) -> bool {
234 false
235 }
236
237 /// Returns `true` if Pro version (WebSocket) is supported.
238 pub fn pro(&self) -> bool {
239 true
240 }
241
242 /// Returns the rate limit in requests per second.
243 pub fn rate_limit(&self) -> u32 {
244 // HyperLiquid has generous rate limits
245 100
246 }
247
248 /// Returns `true` if sandbox/testnet mode is enabled.
249 ///
250 /// Sandbox mode is enabled when either:
251 /// - `config.sandbox` is set to `true`
252 /// - `options.testnet` is set to `true`
253 ///
254 /// # Returns
255 ///
256 /// `true` if sandbox mode is enabled, `false` otherwise.
257 ///
258 /// # Example
259 ///
260 /// ```no_run
261 /// use ccxt_exchanges::hyperliquid::HyperLiquid;
262 ///
263 /// let exchange = HyperLiquid::builder()
264 /// .testnet(true)
265 /// .build()
266 /// .unwrap();
267 /// assert!(exchange.is_sandbox());
268 /// ```
269 pub fn is_sandbox(&self) -> bool {
270 self.base().config.sandbox || self.options.testnet
271 }
272
273 /// Returns the API URLs.
274 ///
275 /// Returns testnet URLs when sandbox mode is enabled (either via
276 /// `config.sandbox` or `options.testnet`), otherwise returns mainnet URLs.
277 pub fn urls(&self) -> HyperLiquidUrls {
278 if self.is_sandbox() {
279 HyperLiquidUrls::testnet()
280 } else {
281 HyperLiquidUrls::mainnet()
282 }
283 }
284
285 /// Returns the wallet address if authenticated.
286 pub fn wallet_address(&self) -> Option<&str> {
287 self.auth
288 .as_ref()
289 .map(auth::HyperLiquidAuth::wallet_address)
290 }
291
292 // TODO: Implement in task 11 (WebSocket Implementation)
293 // /// Creates a public WebSocket client.
294 // pub fn create_ws(&self) -> ws::HyperLiquidWs {
295 // let urls = self.urls();
296 // ws::HyperLiquidWs::new(urls.ws)
297 // }
298}
299
300/// HyperLiquid API URLs.
301#[derive(Debug, Clone)]
302pub struct HyperLiquidUrls {
303 /// REST API base URL.
304 pub rest: String,
305 /// WebSocket URL.
306 pub ws: String,
307}
308
309impl HyperLiquidUrls {
310 /// Returns mainnet environment URLs.
311 pub fn mainnet() -> Self {
312 Self {
313 rest: "https://api.hyperliquid.xyz".to_string(),
314 ws: "wss://api.hyperliquid.xyz/ws".to_string(),
315 }
316 }
317
318 /// Returns testnet environment URLs.
319 pub fn testnet() -> Self {
320 Self {
321 rest: "https://api.hyperliquid-testnet.xyz".to_string(),
322 ws: "wss://api.hyperliquid-testnet.xyz/ws".to_string(),
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_default_options() {
333 let options = HyperLiquidOptions::default();
334 assert!(!options.testnet);
335 assert!(options.vault_address.is_none());
336 assert_eq!(options.default_leverage, 1);
337 // HyperLiquid only supports perpetual futures, so default_type must be Swap
338 assert_eq!(options.default_type, DefaultType::Swap);
339 }
340
341 #[test]
342 fn test_mainnet_urls() {
343 let urls = HyperLiquidUrls::mainnet();
344 assert_eq!(urls.rest, "https://api.hyperliquid.xyz");
345 assert_eq!(urls.ws, "wss://api.hyperliquid.xyz/ws");
346 }
347
348 #[test]
349 fn test_testnet_urls() {
350 let urls = HyperLiquidUrls::testnet();
351 assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
352 assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
353 }
354
355 #[test]
356 fn test_is_sandbox_with_options_testnet() {
357 let config = ExchangeConfig::default();
358 let options = HyperLiquidOptions {
359 testnet: true,
360 ..Default::default()
361 };
362 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
363 assert!(exchange.is_sandbox());
364 }
365
366 #[test]
367 fn test_is_sandbox_with_config_sandbox() {
368 let config = ExchangeConfig {
369 sandbox: true,
370 ..Default::default()
371 };
372 let options = HyperLiquidOptions::default();
373 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
374 assert!(exchange.is_sandbox());
375 }
376
377 #[test]
378 fn test_is_sandbox_false_by_default() {
379 let config = ExchangeConfig::default();
380 let options = HyperLiquidOptions::default();
381 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
382 assert!(!exchange.is_sandbox());
383 }
384
385 #[test]
386 fn test_urls_returns_mainnet_by_default() {
387 let config = ExchangeConfig::default();
388 let options = HyperLiquidOptions::default();
389 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
390 let urls = exchange.urls();
391 assert_eq!(urls.rest, "https://api.hyperliquid.xyz");
392 assert_eq!(urls.ws, "wss://api.hyperliquid.xyz/ws");
393 }
394
395 #[test]
396 fn test_urls_returns_testnet_with_options_testnet() {
397 let config = ExchangeConfig::default();
398 let options = HyperLiquidOptions {
399 testnet: true,
400 ..Default::default()
401 };
402 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
403 let urls = exchange.urls();
404 assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
405 assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
406 }
407
408 #[test]
409 fn test_urls_returns_testnet_with_config_sandbox() {
410 let config = ExchangeConfig {
411 sandbox: true,
412 ..Default::default()
413 };
414 let options = HyperLiquidOptions::default();
415 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
416 let urls = exchange.urls();
417 assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
418 assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
419 }
420}