Skip to main content

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}