ccxt_exchanges/hyperliquid/
endpoint_router.rs

1//! HyperLiquid-specific endpoint router trait.
2//!
3//! This module provides the `HyperLiquidEndpointRouter` trait for routing API requests
4//! to the correct HyperLiquid endpoints.
5//!
6//! # HyperLiquid API Structure
7//!
8//! HyperLiquid uses the simplest endpoint structure among all supported exchanges:
9//! - **REST**: `api.hyperliquid.xyz` - Single REST endpoint for all operations
10//! - **WebSocket**: `api.hyperliquid.xyz/ws` - Single WebSocket endpoint
11//!
12//! Unlike centralized exchanges, HyperLiquid is a decentralized perpetual futures
13//! exchange built on its own L1 blockchain. It uses:
14//! - Ethereum wallet private keys for authentication (EIP-712 typed data signatures)
15//! - Wallet addresses as account identifiers (no registration required)
16//! - USDC as the sole settlement currency
17//!
18//! # Testnet/Sandbox Mode
19//!
20//! HyperLiquid provides a separate testnet environment:
21//! - REST: `api.hyperliquid-testnet.xyz`
22//! - WebSocket: `api.hyperliquid-testnet.xyz/ws`
23//!
24//! # Example
25//!
26//! ```rust,no_run
27//! use ccxt_exchanges::hyperliquid::{HyperLiquid, HyperLiquidEndpointRouter};
28//!
29//! let exchange = HyperLiquid::builder()
30//!     .testnet(false)
31//!     .build()
32//!     .unwrap();
33//!
34//! // Get REST endpoint
35//! let rest_url = exchange.rest_endpoint();
36//! assert!(rest_url.contains("api.hyperliquid.xyz"));
37//!
38//! // Get WebSocket endpoint
39//! let ws_url = exchange.ws_endpoint();
40//! assert!(ws_url.contains("api.hyperliquid.xyz/ws"));
41//! ```
42
43/// HyperLiquid-specific endpoint router trait.
44///
45/// This trait defines methods for obtaining the correct API endpoints for HyperLiquid.
46/// HyperLiquid uses the simplest structure with a single REST and WebSocket endpoint.
47///
48/// # Implementation Notes
49///
50/// - REST API uses a single domain for all operations (`/info` for public, `/exchange` for private)
51/// - WebSocket has a single endpoint for all data streams
52/// - Authentication is handled via EIP-712 signatures, not separate endpoints
53/// - Testnet mode switches to completely different domains
54///
55/// # Sandbox/Testnet Mode
56///
57/// When sandbox mode is enabled (via `config.sandbox` or `options.testnet`):
58/// - REST URL changes to `api.hyperliquid-testnet.xyz`
59/// - WebSocket URL changes to `api.hyperliquid-testnet.xyz/ws`
60pub trait HyperLiquidEndpointRouter {
61    /// Returns the REST API endpoint.
62    ///
63    /// HyperLiquid uses a single REST domain for all operations.
64    /// The operation type is specified in the API path:
65    /// - `/info` - Public data requests
66    /// - `/exchange` - Private/authenticated requests
67    ///
68    /// # Returns
69    ///
70    /// The REST API base URL string.
71    ///
72    /// # Production vs Testnet
73    ///
74    /// - Production: `https://api.hyperliquid.xyz`
75    /// - Testnet: `https://api.hyperliquid-testnet.xyz`
76    ///
77    /// # Example
78    ///
79    /// ```rust,no_run
80    /// use ccxt_exchanges::hyperliquid::{HyperLiquid, HyperLiquidEndpointRouter};
81    ///
82    /// let exchange = HyperLiquid::builder().build().unwrap();
83    /// let url = exchange.rest_endpoint();
84    /// assert_eq!(url, "https://api.hyperliquid.xyz");
85    /// ```
86    fn rest_endpoint(&self) -> &'static str;
87
88    /// Returns the WebSocket endpoint.
89    ///
90    /// HyperLiquid uses a single WebSocket endpoint for all data streams.
91    /// Unlike other exchanges, there is no separation between public and private
92    /// WebSocket connections - authentication is handled at the message level.
93    ///
94    /// # Returns
95    ///
96    /// The complete WebSocket URL.
97    ///
98    /// # Production vs Testnet
99    ///
100    /// - Production: `wss://api.hyperliquid.xyz/ws`
101    /// - Testnet: `wss://api.hyperliquid-testnet.xyz/ws`
102    ///
103    /// # Example
104    ///
105    /// ```rust,no_run
106    /// use ccxt_exchanges::hyperliquid::{HyperLiquid, HyperLiquidEndpointRouter};
107    ///
108    /// let exchange = HyperLiquid::builder().build().unwrap();
109    /// let url = exchange.ws_endpoint();
110    /// assert_eq!(url, "wss://api.hyperliquid.xyz/ws");
111    /// ```
112    fn ws_endpoint(&self) -> &str;
113}
114
115use super::HyperLiquid;
116
117impl HyperLiquidEndpointRouter for HyperLiquid {
118    fn rest_endpoint(&self) -> &'static str {
119        // Return static reference based on sandbox mode
120        // is_sandbox() checks both config.sandbox and options.testnet
121        if self.is_sandbox() {
122            "https://api.hyperliquid-testnet.xyz"
123        } else {
124            "https://api.hyperliquid.xyz"
125        }
126    }
127
128    fn ws_endpoint(&self) -> &str {
129        // Return static reference based on sandbox mode
130        // is_sandbox() checks both config.sandbox and options.testnet
131        if self.is_sandbox() {
132            "wss://api.hyperliquid-testnet.xyz/ws"
133        } else {
134            "wss://api.hyperliquid.xyz/ws"
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::hyperliquid::HyperLiquidOptions;
143    use ccxt_core::ExchangeConfig;
144
145    fn create_test_hyperliquid() -> HyperLiquid {
146        HyperLiquid::builder().build().unwrap()
147    }
148
149    fn create_testnet_hyperliquid() -> HyperLiquid {
150        HyperLiquid::builder().testnet(true).build().unwrap()
151    }
152
153    fn create_sandbox_hyperliquid() -> HyperLiquid {
154        let config = ExchangeConfig {
155            sandbox: true,
156            ..Default::default()
157        };
158        let options = HyperLiquidOptions::default();
159        HyperLiquid::new_with_options(config, options, None).unwrap()
160    }
161
162    // ==================== REST Endpoint Tests ====================
163
164    #[test]
165    fn test_rest_endpoint_production() {
166        let exchange = create_test_hyperliquid();
167        let url = exchange.rest_endpoint();
168        assert_eq!(url, "https://api.hyperliquid.xyz");
169        assert!(!url.contains("testnet"));
170    }
171
172    #[test]
173    fn test_rest_endpoint_testnet() {
174        let exchange = create_testnet_hyperliquid();
175        let url = exchange.rest_endpoint();
176        assert_eq!(url, "https://api.hyperliquid-testnet.xyz");
177    }
178
179    #[test]
180    fn test_rest_endpoint_sandbox() {
181        let exchange = create_sandbox_hyperliquid();
182        let url = exchange.rest_endpoint();
183        assert_eq!(url, "https://api.hyperliquid-testnet.xyz");
184    }
185
186    // ==================== WebSocket Endpoint Tests ====================
187
188    #[test]
189    fn test_ws_endpoint_production() {
190        let exchange = create_test_hyperliquid();
191        let url = exchange.ws_endpoint();
192        assert_eq!(url, "wss://api.hyperliquid.xyz/ws");
193        assert!(!url.contains("testnet"));
194    }
195
196    #[test]
197    fn test_ws_endpoint_testnet() {
198        let exchange = create_testnet_hyperliquid();
199        let url = exchange.ws_endpoint();
200        assert_eq!(url, "wss://api.hyperliquid-testnet.xyz/ws");
201    }
202
203    #[test]
204    fn test_ws_endpoint_sandbox() {
205        let exchange = create_sandbox_hyperliquid();
206        let url = exchange.ws_endpoint();
207        assert_eq!(url, "wss://api.hyperliquid-testnet.xyz/ws");
208    }
209
210    // ==================== Consistency Tests ====================
211
212    #[test]
213    fn test_rest_endpoint_consistency_with_urls() {
214        let exchange = create_test_hyperliquid();
215        let router_url = exchange.rest_endpoint();
216        let urls_rest = &exchange.urls().rest;
217        assert_eq!(router_url, urls_rest);
218    }
219
220    #[test]
221    fn test_ws_endpoint_consistency_with_urls() {
222        let exchange = create_test_hyperliquid();
223        let router_url = exchange.ws_endpoint();
224        let urls_ws = &exchange.urls().ws;
225        assert_eq!(router_url, urls_ws);
226    }
227
228    #[test]
229    fn test_testnet_rest_endpoint_consistency_with_urls() {
230        let exchange = create_testnet_hyperliquid();
231        let router_url = exchange.rest_endpoint();
232        let urls_rest = &exchange.urls().rest;
233        assert_eq!(router_url, urls_rest);
234    }
235
236    #[test]
237    fn test_testnet_ws_endpoint_consistency_with_urls() {
238        let exchange = create_testnet_hyperliquid();
239        let router_url = exchange.ws_endpoint();
240        let urls_ws = &exchange.urls().ws;
241        assert_eq!(router_url, urls_ws);
242    }
243
244    // ==================== URL Format Tests ====================
245
246    #[test]
247    fn test_rest_endpoint_uses_https() {
248        let exchange = create_test_hyperliquid();
249        let url = exchange.rest_endpoint();
250        assert!(url.starts_with("https://"));
251    }
252
253    #[test]
254    fn test_ws_endpoint_uses_wss() {
255        let exchange = create_test_hyperliquid();
256        let url = exchange.ws_endpoint();
257        assert!(url.starts_with("wss://"));
258    }
259
260    #[test]
261    fn test_ws_endpoint_contains_ws_path() {
262        let exchange = create_test_hyperliquid();
263        let url = exchange.ws_endpoint();
264        assert!(url.ends_with("/ws"));
265    }
266
267    // ==================== Sandbox Mode Tests ====================
268
269    #[test]
270    fn test_is_sandbox_with_testnet_option() {
271        let exchange = create_testnet_hyperliquid();
272        assert!(exchange.is_sandbox());
273    }
274
275    #[test]
276    fn test_is_sandbox_with_config_sandbox() {
277        let exchange = create_sandbox_hyperliquid();
278        assert!(exchange.is_sandbox());
279    }
280
281    #[test]
282    fn test_is_not_sandbox_by_default() {
283        let exchange = create_test_hyperliquid();
284        assert!(!exchange.is_sandbox());
285    }
286}