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 error;
54mod exchange_impl;
55pub mod parser;
56pub mod rest;
57pub mod ws;
58mod ws_exchange_impl;
59
60pub use auth::HyperLiquidAuth;
61pub use builder::{HyperLiquidBuilder, validate_default_type};
62pub use error::{HyperLiquidErrorCode, is_error_response, parse_error};
63
64/// HyperLiquid exchange structure.
65#[derive(Debug)]
66pub struct HyperLiquid {
67    /// Base exchange instance.
68    base: BaseExchange,
69    /// HyperLiquid-specific options.
70    options: HyperLiquidOptions,
71    /// Authentication instance (optional, for private API).
72    auth: Option<HyperLiquidAuth>,
73}
74
75/// HyperLiquid-specific options.
76///
77/// Note: HyperLiquid only supports perpetual futures (Swap). The `default_type`
78/// field defaults to `Swap` and attempting to set it to any other value will
79/// result in a validation error.
80#[derive(Debug, Clone)]
81pub struct HyperLiquidOptions {
82    /// Whether to use testnet.
83    pub testnet: bool,
84    /// Vault address for vault trading (optional).
85    pub vault_address: Option<String>,
86    /// Default leverage multiplier.
87    pub default_leverage: u32,
88    /// Default market type for trading.
89    ///
90    /// HyperLiquid only supports perpetual futures, so this must be `Swap`.
91    /// Attempting to set any other value will result in a validation error.
92    pub default_type: DefaultType,
93}
94
95impl Default for HyperLiquidOptions {
96    fn default() -> Self {
97        Self {
98            testnet: false,
99            vault_address: None,
100            default_leverage: 1,
101            // HyperLiquid only supports perpetual futures (Swap)
102            default_type: DefaultType::Swap,
103        }
104    }
105}
106
107impl HyperLiquid {
108    /// Creates a new HyperLiquid instance using the builder pattern.
109    ///
110    /// This is the recommended way to create a HyperLiquid instance.
111    ///
112    /// # Example
113    ///
114    /// ```no_run
115    /// use ccxt_exchanges::hyperliquid::HyperLiquid;
116    ///
117    /// let exchange = HyperLiquid::builder()
118    ///     .private_key("0x...")
119    ///     .testnet(true)
120    ///     .build()
121    ///     .unwrap();
122    /// ```
123    pub fn builder() -> HyperLiquidBuilder {
124        HyperLiquidBuilder::new()
125    }
126
127    /// Creates a new HyperLiquid instance with custom options.
128    ///
129    /// This is used internally by the builder pattern.
130    ///
131    /// # Arguments
132    ///
133    /// * `config` - Exchange configuration.
134    /// * `options` - HyperLiquid-specific options.
135    /// * `auth` - Optional authentication instance.
136    pub fn new_with_options(
137        config: ExchangeConfig,
138        options: HyperLiquidOptions,
139        auth: Option<HyperLiquidAuth>,
140    ) -> Result<Self> {
141        let base = BaseExchange::new(config)?;
142        Ok(Self {
143            base,
144            options,
145            auth,
146        })
147    }
148
149    /// Returns a reference to the base exchange.
150    pub fn base(&self) -> &BaseExchange {
151        &self.base
152    }
153
154    /// Returns a mutable reference to the base exchange.
155    pub fn base_mut(&mut self) -> &mut BaseExchange {
156        &mut self.base
157    }
158
159    /// Returns the HyperLiquid options.
160    pub fn options(&self) -> &HyperLiquidOptions {
161        &self.options
162    }
163
164    /// Returns a reference to the authentication instance.
165    pub fn auth(&self) -> Option<&HyperLiquidAuth> {
166        self.auth.as_ref()
167    }
168
169    /// Returns the exchange ID.
170    pub fn id(&self) -> &str {
171        "hyperliquid"
172    }
173
174    /// Returns the exchange name.
175    pub fn name(&self) -> &str {
176        "HyperLiquid"
177    }
178
179    /// Returns the API version.
180    pub fn version(&self) -> &str {
181        "1"
182    }
183
184    /// Returns `true` if the exchange is CCXT-certified.
185    pub fn certified(&self) -> bool {
186        false
187    }
188
189    /// Returns `true` if Pro version (WebSocket) is supported.
190    pub fn pro(&self) -> bool {
191        true
192    }
193
194    /// Returns the rate limit in requests per second.
195    pub fn rate_limit(&self) -> u32 {
196        // HyperLiquid has generous rate limits
197        100
198    }
199
200    /// Returns `true` if sandbox/testnet mode is enabled.
201    ///
202    /// Sandbox mode is enabled when either:
203    /// - `config.sandbox` is set to `true`
204    /// - `options.testnet` is set to `true`
205    ///
206    /// # Returns
207    ///
208    /// `true` if sandbox mode is enabled, `false` otherwise.
209    ///
210    /// # Example
211    ///
212    /// ```no_run
213    /// use ccxt_exchanges::hyperliquid::HyperLiquid;
214    ///
215    /// let exchange = HyperLiquid::builder()
216    ///     .testnet(true)
217    ///     .build()
218    ///     .unwrap();
219    /// assert!(exchange.is_sandbox());
220    /// ```
221    pub fn is_sandbox(&self) -> bool {
222        self.base().config.sandbox || self.options.testnet
223    }
224
225    /// Returns the API URLs.
226    ///
227    /// Returns testnet URLs when sandbox mode is enabled (either via
228    /// `config.sandbox` or `options.testnet`), otherwise returns mainnet URLs.
229    pub fn urls(&self) -> HyperLiquidUrls {
230        if self.is_sandbox() {
231            HyperLiquidUrls::testnet()
232        } else {
233            HyperLiquidUrls::mainnet()
234        }
235    }
236
237    /// Returns the wallet address if authenticated.
238    pub fn wallet_address(&self) -> Option<&str> {
239        self.auth.as_ref().map(|a| a.wallet_address())
240    }
241
242    // TODO: Implement in task 11 (WebSocket Implementation)
243    // /// Creates a public WebSocket client.
244    // pub fn create_ws(&self) -> ws::HyperLiquidWs {
245    //     let urls = self.urls();
246    //     ws::HyperLiquidWs::new(urls.ws)
247    // }
248}
249
250/// HyperLiquid API URLs.
251#[derive(Debug, Clone)]
252pub struct HyperLiquidUrls {
253    /// REST API base URL.
254    pub rest: String,
255    /// WebSocket URL.
256    pub ws: String,
257}
258
259impl HyperLiquidUrls {
260    /// Returns mainnet environment URLs.
261    pub fn mainnet() -> Self {
262        Self {
263            rest: "https://api.hyperliquid.xyz".to_string(),
264            ws: "wss://api.hyperliquid.xyz/ws".to_string(),
265        }
266    }
267
268    /// Returns testnet environment URLs.
269    pub fn testnet() -> Self {
270        Self {
271            rest: "https://api.hyperliquid-testnet.xyz".to_string(),
272            ws: "wss://api.hyperliquid-testnet.xyz/ws".to_string(),
273        }
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_default_options() {
283        let options = HyperLiquidOptions::default();
284        assert!(!options.testnet);
285        assert!(options.vault_address.is_none());
286        assert_eq!(options.default_leverage, 1);
287        // HyperLiquid only supports perpetual futures, so default_type must be Swap
288        assert_eq!(options.default_type, DefaultType::Swap);
289    }
290
291    #[test]
292    fn test_mainnet_urls() {
293        let urls = HyperLiquidUrls::mainnet();
294        assert_eq!(urls.rest, "https://api.hyperliquid.xyz");
295        assert_eq!(urls.ws, "wss://api.hyperliquid.xyz/ws");
296    }
297
298    #[test]
299    fn test_testnet_urls() {
300        let urls = HyperLiquidUrls::testnet();
301        assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
302        assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
303    }
304
305    #[test]
306    fn test_is_sandbox_with_options_testnet() {
307        let config = ExchangeConfig::default();
308        let options = HyperLiquidOptions {
309            testnet: true,
310            ..Default::default()
311        };
312        let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
313        assert!(exchange.is_sandbox());
314    }
315
316    #[test]
317    fn test_is_sandbox_with_config_sandbox() {
318        let config = ExchangeConfig {
319            sandbox: true,
320            ..Default::default()
321        };
322        let options = HyperLiquidOptions::default();
323        let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
324        assert!(exchange.is_sandbox());
325    }
326
327    #[test]
328    fn test_is_sandbox_false_by_default() {
329        let config = ExchangeConfig::default();
330        let options = HyperLiquidOptions::default();
331        let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
332        assert!(!exchange.is_sandbox());
333    }
334
335    #[test]
336    fn test_urls_returns_mainnet_by_default() {
337        let config = ExchangeConfig::default();
338        let options = HyperLiquidOptions::default();
339        let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
340        let urls = exchange.urls();
341        assert_eq!(urls.rest, "https://api.hyperliquid.xyz");
342        assert_eq!(urls.ws, "wss://api.hyperliquid.xyz/ws");
343    }
344
345    #[test]
346    fn test_urls_returns_testnet_with_options_testnet() {
347        let config = ExchangeConfig::default();
348        let options = HyperLiquidOptions {
349            testnet: true,
350            ..Default::default()
351        };
352        let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
353        let urls = exchange.urls();
354        assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
355        assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
356    }
357
358    #[test]
359    fn test_urls_returns_testnet_with_config_sandbox() {
360        let config = ExchangeConfig {
361            sandbox: true,
362            ..Default::default()
363        };
364        let options = HyperLiquidOptions::default();
365        let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
366        let urls = exchange.urls();
367        assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
368        assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
369    }
370}