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}