ccxt_exchanges/hyperliquid/
rest.rs

1//! HyperLiquid REST API implementation.
2//!
3//! Implements all REST API endpoint operations for the HyperLiquid exchange.
4
5use super::{HyperLiquid, error, parser};
6use ccxt_core::{
7    Error, ParseError, Result,
8    types::{Balance, Market, Order, OrderBook, OrderSide, OrderType, Ticker, Trade},
9};
10use rust_decimal::Decimal;
11use serde_json::{Map, Value};
12use std::{collections::HashMap, sync::Arc};
13use tracing::{debug, info, warn};
14
15impl HyperLiquid {
16    // ============================================================================
17    // Helper Methods
18    // ============================================================================
19
20    /// Make a public info API request.
21    pub(crate) async fn info_request(&self, request_type: &str, payload: Value) -> Result<Value> {
22        let urls = self.urls();
23        let url = format!("{}/info", urls.rest);
24
25        // Build the request body by merging type with payload
26        let body = if let Value::Object(map) = payload {
27            let mut obj = serde_json::Map::new();
28            obj.insert("type".to_string(), Value::String(request_type.to_string()));
29            for (k, v) in map {
30                obj.insert(k, v);
31            }
32            Value::Object(obj)
33        } else {
34            let mut map = serde_json::Map::new();
35            map.insert(
36                "type".to_string(),
37                serde_json::Value::String(request_type.to_string()),
38            );
39            serde_json::Value::Object(map)
40        };
41
42        debug!("HyperLiquid info request: {} {:?}", request_type, body);
43
44        let response = self.base().http_client.post(&url, None, Some(body)).await?;
45
46        if error::is_error_response(&response) {
47            return Err(error::parse_error(&response));
48        }
49
50        Ok(response)
51    }
52
53    /// Make an exchange action request (requires authentication).
54    pub(crate) async fn exchange_request(&self, action: Value, nonce: u64) -> Result<Value> {
55        let auth = self
56            .auth
57            .as_ref()
58            .ok_or_else(|| Error::authentication("Private key required for exchange actions"))?;
59
60        let urls = self.urls();
61        let url = format!("{}/exchange", urls.rest);
62        let is_mainnet = !self.options.testnet;
63
64        // Sign the action
65        let signature = auth.sign_l1_action(&action, nonce, is_mainnet)?;
66
67        let mut signature_map = serde_json::Map::new();
68        signature_map.insert(
69            "r".to_string(),
70            serde_json::Value::String(format!("0x{}", signature.r)),
71        );
72        signature_map.insert(
73            "s".to_string(),
74            serde_json::Value::String(format!("0x{}", signature.s)),
75        );
76        signature_map.insert(
77            "v".to_string(),
78            serde_json::Value::Number(signature.v.into()),
79        );
80
81        let mut body_map = serde_json::Map::new();
82        body_map.insert("action".to_string(), action.clone());
83        body_map.insert("nonce".to_string(), serde_json::Value::Number(nonce.into()));
84        body_map.insert(
85            "signature".to_string(),
86            serde_json::Value::Object(signature_map),
87        );
88        if let Some(vault_address) = &self.options.vault_address {
89            body_map.insert(
90                "vaultAddress".to_string(),
91                serde_json::Value::String(format!("0x{}", hex::encode(vault_address))),
92            );
93        }
94        let body = serde_json::Value::Object(body_map);
95
96        debug!("HyperLiquid exchange request: {:?}", action);
97
98        let response = self.base().http_client.post(&url, None, Some(body)).await?;
99
100        if error::is_error_response(&response) {
101            return Err(error::parse_error(&response));
102        }
103
104        Ok(response)
105    }
106
107    /// Get current timestamp as nonce.
108    fn get_nonce(&self) -> u64 {
109        chrono::Utc::now().timestamp_millis() as u64
110    }
111
112    // ============================================================================
113    // Public API Methods - Market Data
114    // ============================================================================
115
116    /// Fetch all trading markets.
117    pub async fn fetch_markets(&self) -> Result<HashMap<String, Arc<Market>>> {
118        let response = self
119            .info_request("meta", serde_json::Value::Object(serde_json::Map::new()))
120            .await?;
121
122        let universe = response["universe"]
123            .as_array()
124            .ok_or_else(|| Error::from(ParseError::missing_field("universe")))?;
125
126        let mut markets = Vec::new();
127        for (index, asset) in universe.iter().enumerate() {
128            match parser::parse_market(asset, index) {
129                Ok(market) => markets.push(market),
130                Err(e) => {
131                    warn!(error = %e, "Failed to parse market");
132                }
133            }
134        }
135
136        // Cache the markets and preserve ownership for the caller
137        let result = self.base().set_markets(markets, None).await?;
138
139        info!("Loaded {} markets for HyperLiquid", result.len());
140        Ok(result)
141    }
142
143    /// Load and cache market data.
144    pub async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Arc<Market>>> {
145        // Acquire the loading lock to serialize concurrent load_markets calls
146        // This prevents multiple tasks from making duplicate API calls
147        let _loading_guard = self.base().market_loading_lock.lock().await;
148
149        // Check cache status while holding the lock
150        {
151            let cache = self.base().market_cache.read().await;
152            if cache.loaded && !reload {
153                debug!(
154                    "Returning cached markets for HyperLiquid ({} markets)",
155                    cache.markets.len()
156                );
157                return Ok(cache.markets.clone());
158            }
159        }
160
161        info!("Loading markets for HyperLiquid (reload: {})", reload);
162        let _markets = self.fetch_markets().await?;
163
164        let cache = self.base().market_cache.read().await;
165        Ok(cache.markets.clone())
166    }
167
168    /// Fetch ticker for a single trading pair.
169    pub async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
170        let market = self.base().market(symbol).await?;
171        let response = self
172            .info_request("allMids", serde_json::Value::Object(serde_json::Map::new()))
173            .await?;
174
175        // Response is a map of asset name to mid price
176        let mid_price = response[&market.base]
177            .as_str()
178            .and_then(|s| s.parse::<Decimal>().ok())
179            .ok_or_else(|| Error::bad_symbol(format!("No ticker data for {}", symbol)))?;
180
181        parser::parse_ticker(symbol, mid_price, Some(&market))
182    }
183
184    /// Fetch tickers for multiple trading pairs.
185    pub async fn fetch_tickers(&self, symbols: Option<Vec<String>>) -> Result<Vec<Ticker>> {
186        let response = self
187            .info_request("allMids", serde_json::Value::Object(serde_json::Map::new()))
188            .await?;
189
190        let cache = self.base().market_cache.read().await;
191        if !cache.loaded {
192            drop(cache);
193            return Err(Error::exchange(
194                "-1",
195                "Markets not loaded. Call load_markets() first.",
196            ));
197        }
198
199        let mut tickers = Vec::new();
200
201        if let Some(obj) = response.as_object() {
202            for (asset, price) in obj {
203                if let Some(mid_price) = price.as_str().and_then(|s| s.parse::<Decimal>().ok()) {
204                    let symbol = format!("{}/USDC:USDC", asset);
205
206                    // Filter by requested symbols if provided
207                    if let Some(ref syms) = symbols {
208                        if !syms.contains(&symbol) {
209                            continue;
210                        }
211                    }
212
213                    if let Ok(ticker) = parser::parse_ticker(&symbol, mid_price, None) {
214                        tickers.push(ticker);
215                    }
216                }
217            }
218        }
219
220        Ok(tickers)
221    }
222
223    /// Fetch order book for a trading pair.
224    pub async fn fetch_order_book(&self, symbol: &str, _limit: Option<u32>) -> Result<OrderBook> {
225        let market = self.base().market(symbol).await?;
226
227        let response = self
228            .info_request("l2Book", {
229                let mut map = serde_json::Map::new();
230                map.insert(
231                    "coin".to_string(),
232                    serde_json::Value::String(market.base.clone()),
233                );
234                serde_json::Value::Object(map)
235            })
236            .await?;
237
238        parser::parse_orderbook(&response, symbol.to_string())
239    }
240
241    /// Fetch recent public trades.
242    pub async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
243        let market = self.base().market(symbol).await?;
244        let limit = limit.unwrap_or(100).min(1000);
245
246        let response = self
247            .info_request("recentTrades", {
248                let mut map = serde_json::Map::new();
249                map.insert(
250                    "coin".to_string(),
251                    serde_json::Value::String(market.base.clone()),
252                );
253                map.insert("n".to_string(), serde_json::Value::Number(limit.into()));
254                serde_json::Value::Object(map)
255            })
256            .await?;
257
258        let trades_array = response
259            .as_array()
260            .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Expected array")))?;
261
262        let mut trades = Vec::new();
263        for trade_data in trades_array {
264            match parser::parse_trade(trade_data, Some(&market)) {
265                Ok(trade) => trades.push(trade),
266                Err(e) => {
267                    warn!(error = %e, "Failed to parse trade");
268                }
269            }
270        }
271
272        Ok(trades)
273    }
274
275    /// Fetch OHLCV (candlestick) data.
276    pub async fn fetch_ohlcv(
277        &self,
278        symbol: &str,
279        timeframe: &str,
280        since: Option<i64>,
281        limit: Option<u32>,
282    ) -> Result<Vec<ccxt_core::types::Ohlcv>> {
283        let market = self.base().market(symbol).await?;
284        let limit = limit.unwrap_or(500).min(5000) as i64;
285
286        // Convert timeframe to HyperLiquid interval format
287        let interval = match timeframe {
288            "1m" => "1m",
289            "5m" => "5m",
290            "15m" => "15m",
291            "30m" => "30m",
292            "1h" => "1h",
293            "4h" => "4h",
294            "1d" => "1d",
295            "1w" => "1w",
296            _ => "1h",
297        };
298
299        let now = chrono::Utc::now().timestamp_millis() as u64;
300        let start_time = since
301            .map(|s| s as u64)
302            .unwrap_or(now - limit as u64 * 3600000); // Default to limit hours ago
303        let end_time = now;
304
305        let response = self
306            .info_request("candleSnapshot", {
307                let mut map = serde_json::Map::new();
308                map.insert(
309                    "coin".to_string(),
310                    serde_json::Value::String(market.base.clone()),
311                );
312                map.insert(
313                    "interval".to_string(),
314                    serde_json::Value::String(interval.to_string()),
315                );
316                map.insert(
317                    "startTime".to_string(),
318                    serde_json::Value::Number(start_time.into()),
319                );
320                map.insert(
321                    "endTime".to_string(),
322                    serde_json::Value::Number(end_time.into()),
323                );
324                serde_json::Value::Object(map)
325            })
326            .await?;
327
328        let candles_array = response
329            .as_array()
330            .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Expected array")))?;
331
332        let mut ohlcv_list = Vec::new();
333        for candle_data in candles_array {
334            match parser::parse_ohlcv(candle_data) {
335                Ok(ohlcv) => ohlcv_list.push(ohlcv),
336                Err(e) => {
337                    warn!(error = %e, "Failed to parse OHLCV");
338                }
339            }
340        }
341
342        Ok(ohlcv_list)
343    }
344
345    /// Fetch current funding rate for a symbol.
346    pub async fn fetch_funding_rate(&self, symbol: &str) -> Result<ccxt_core::types::FundingRate> {
347        let market = self.base().market(symbol).await?;
348
349        let response = self
350            .info_request("meta", serde_json::Value::Object(serde_json::Map::new()))
351            .await?;
352
353        // Find the asset in the universe
354        let universe = response["universe"]
355            .as_array()
356            .ok_or_else(|| Error::from(ParseError::missing_field("universe")))?;
357
358        let asset_index: usize = market.id.parse().unwrap_or(0);
359        let asset_data = universe
360            .get(asset_index)
361            .ok_or_else(|| Error::bad_symbol(format!("Asset not found: {}", symbol)))?;
362
363        let funding_rate = asset_data["funding"]
364            .as_str()
365            .and_then(|s| s.parse::<f64>().ok())
366            .unwrap_or(0.0);
367
368        let timestamp = chrono::Utc::now().timestamp_millis();
369
370        Ok(ccxt_core::types::FundingRate {
371            info: serde_json::json!({}),
372            symbol: symbol.to_string(),
373            mark_price: None,
374            index_price: None,
375            interest_rate: None,
376            estimated_settle_price: None,
377            funding_rate: Some(funding_rate),
378            funding_timestamp: None,
379            funding_datetime: None,
380            previous_funding_rate: None,
381            previous_funding_timestamp: None,
382            previous_funding_datetime: None,
383            timestamp: Some(timestamp),
384            datetime: parser::timestamp_to_datetime(timestamp),
385        })
386    }
387
388    // ============================================================================
389    // Private API Methods - Account
390    // ============================================================================
391
392    /// Fetch account balance.
393    pub async fn fetch_balance(&self) -> Result<Balance> {
394        let address = self
395            .wallet_address()
396            .ok_or_else(|| Error::authentication("Private key required to fetch balance"))?;
397
398        let response = self
399            .info_request("clearinghouseState", {
400                let mut map = Map::new();
401                map.insert("user".to_string(), Value::String(address.to_string()));
402                Value::Object(map)
403            })
404            .await?;
405
406        parser::parse_balance(&response)
407    }
408
409    /// Fetch open positions.
410    pub async fn fetch_positions(
411        &self,
412        symbols: Option<Vec<String>>,
413    ) -> Result<Vec<ccxt_core::types::Position>> {
414        let address = self
415            .wallet_address()
416            .ok_or_else(|| Error::authentication("Private key required to fetch positions"))?;
417
418        let response = self
419            .info_request("clearinghouseState", {
420                let mut map = Map::new();
421                map.insert("user".to_string(), Value::String(address.to_string()));
422                Value::Object(map)
423            })
424            .await?;
425
426        let asset_positions = response["assetPositions"]
427            .as_array()
428            .ok_or_else(|| Error::from(ParseError::missing_field("assetPositions")))?;
429
430        let mut positions = Vec::new();
431        for pos_data in asset_positions {
432            let position = pos_data.get("position").unwrap_or(pos_data);
433
434            let coin = position["coin"].as_str().unwrap_or("");
435            let symbol = format!("{}/USDC:USDC", coin);
436
437            // Filter by symbols if provided
438            if let Some(ref syms) = symbols {
439                if !syms.contains(&symbol) {
440                    continue;
441                }
442            }
443
444            let szi = position["szi"]
445                .as_str()
446                .and_then(|s| s.parse::<f64>().ok())
447                .unwrap_or(0.0);
448
449            // Skip zero positions
450            if szi.abs() < 1e-10 {
451                continue;
452            }
453
454            let entry_px = position["entryPx"]
455                .as_str()
456                .and_then(|s| s.parse::<f64>().ok());
457            let liquidation_px = position["liquidationPx"]
458                .as_str()
459                .and_then(|s| s.parse::<f64>().ok());
460            let unrealized_pnl = position["unrealizedPnl"]
461                .as_str()
462                .and_then(|s| s.parse::<f64>().ok());
463            let margin_used = position["marginUsed"]
464                .as_str()
465                .and_then(|s| s.parse::<f64>().ok());
466
467            let leverage_info = position.get("leverage");
468            let leverage = leverage_info
469                .and_then(|l| l["value"].as_str())
470                .and_then(|s| s.parse::<f64>().ok());
471            let margin_mode = leverage_info
472                .and_then(|l| l["type"].as_str())
473                .map(|t| if t == "cross" { "cross" } else { "isolated" }.to_string());
474
475            let side = if szi > 0.0 { "long" } else { "short" };
476
477            positions.push(ccxt_core::types::Position {
478                info: pos_data.clone(),
479                id: None,
480                symbol,
481                side: Some(side.to_string()),
482                position_side: None,
483                dual_side_position: None,
484                contracts: Some(szi.abs()),
485                contract_size: Some(1.0),
486                entry_price: entry_px,
487                mark_price: None,
488                notional: None,
489                leverage,
490                collateral: margin_used,
491                initial_margin: margin_used,
492                initial_margin_percentage: None,
493                maintenance_margin: None,
494                maintenance_margin_percentage: None,
495                unrealized_pnl,
496                realized_pnl: None,
497                liquidation_price: liquidation_px,
498                margin_ratio: None,
499                margin_mode,
500                hedged: None,
501                percentage: None,
502                timestamp: Some(chrono::Utc::now().timestamp_millis()),
503                datetime: None,
504            });
505        }
506
507        Ok(positions)
508    }
509
510    /// Fetch open orders.
511    pub async fn fetch_open_orders(
512        &self,
513        symbol: Option<&str>,
514        _since: Option<i64>,
515        _limit: Option<u32>,
516    ) -> Result<Vec<Order>> {
517        let address = self
518            .wallet_address()
519            .ok_or_else(|| Error::authentication("Private key required to fetch orders"))?;
520
521        let response = self
522            .info_request("openOrders", {
523                let mut map = Map::new();
524                map.insert("user".to_string(), Value::String(address.to_string()));
525                Value::Object(map)
526            })
527            .await?;
528
529        let orders_array = response
530            .as_array()
531            .ok_or_else(|| Error::from(ParseError::invalid_format("data", "Expected array")))?;
532
533        let mut orders = Vec::new();
534        for order_data in orders_array {
535            match parser::parse_order(order_data, None) {
536                Ok(order) => {
537                    // Filter by symbol if provided
538                    if let Some(sym) = symbol {
539                        if order.symbol != sym {
540                            continue;
541                        }
542                    }
543                    orders.push(order);
544                }
545                Err(e) => {
546                    warn!(error = %e, "Failed to parse order");
547                }
548            }
549        }
550
551        Ok(orders)
552    }
553
554    // ============================================================================
555    // Private API Methods - Order Management
556    // ============================================================================
557
558    /// Create a new order.
559    pub async fn create_order(
560        &self,
561        symbol: &str,
562        order_type: OrderType,
563        side: OrderSide,
564        amount: f64,
565        price: Option<f64>,
566    ) -> Result<Order> {
567        let market = self.base().market(symbol).await?;
568        let asset_index: u32 = market.id.parse().unwrap_or(0);
569
570        let is_buy = matches!(side, OrderSide::Buy);
571        let limit_px = price.unwrap_or(0.0);
572
573        let order_wire = match order_type {
574            OrderType::Market => {
575                let mut limit_map = Map::new();
576                limit_map.insert("tif".to_string(), Value::String("Ioc".to_string()));
577                let mut map = Map::new();
578                map.insert("limit".to_string(), Value::Object(limit_map));
579                Value::Object(map)
580            }
581            OrderType::Limit => {
582                let mut limit_map = Map::new();
583                limit_map.insert("tif".to_string(), Value::String("Gtc".to_string()));
584                let mut map = Map::new();
585                map.insert("limit".to_string(), Value::Object(limit_map));
586                Value::Object(map)
587            }
588            _ => {
589                let mut limit_map = Map::new();
590                limit_map.insert("tif".to_string(), Value::String("Gtc".to_string()));
591                let mut map = Map::new();
592                map.insert("limit".to_string(), Value::Object(limit_map));
593                Value::Object(map)
594            }
595        };
596
597        let action = {
598            let mut order_map = Map::new();
599            order_map.insert("a".to_string(), Value::Number(asset_index.into()));
600            order_map.insert("b".to_string(), Value::Bool(is_buy));
601            order_map.insert("p".to_string(), Value::String(limit_px.to_string()));
602            order_map.insert("s".to_string(), Value::String(amount.to_string()));
603            order_map.insert("r".to_string(), Value::Bool(false));
604            order_map.insert("t".to_string(), order_wire);
605
606            let mut map = Map::new();
607            map.insert("type".to_string(), Value::String("order".to_string()));
608            map.insert(
609                "orders".to_string(),
610                Value::Array(vec![Value::Object(order_map)]),
611            );
612            map.insert("grouping".to_string(), Value::String("na".to_string()));
613            Value::Object(map)
614        };
615
616        let nonce = self.get_nonce();
617        let response = self.exchange_request(action, nonce).await?;
618
619        // Parse response
620        if let Some(statuses) = response["response"]["data"]["statuses"].as_array() {
621            if let Some(status) = statuses.first() {
622                if let Some(resting) = status.get("resting") {
623                    return parser::parse_order(resting, Some(&market));
624                }
625                if let Some(filled) = status.get("filled") {
626                    return parser::parse_order(filled, Some(&market));
627                }
628            }
629        }
630
631        Err(Error::exchange("-1", "Failed to parse order response"))
632    }
633
634    /// Cancel an order.
635    pub async fn cancel_order(&self, id: &str, symbol: &str) -> Result<Order> {
636        let market = self.base().market(symbol).await?;
637        let asset_index: u32 = market.id.parse().unwrap_or(0);
638        let order_id: u64 = id
639            .parse()
640            .map_err(|_| Error::invalid_request("Invalid order ID format"))?;
641
642        let action = {
643            let mut cancel_map = Map::new();
644            cancel_map.insert("a".to_string(), asset_index.into());
645            cancel_map.insert("o".to_string(), order_id.into());
646
647            let mut map = Map::new();
648            map.insert("type".to_string(), Value::String("cancel".to_string()));
649            map.insert(
650                "cancels".to_string(),
651                Value::Array(vec![Value::Object(cancel_map)]),
652            );
653            Value::Object(map)
654        };
655
656        let nonce = self.get_nonce();
657        let _response = self.exchange_request(action, nonce).await?;
658
659        // Return a minimal order object indicating cancellation
660        Ok(Order::new(
661            id.to_string(),
662            symbol.to_string(),
663            OrderType::Limit,
664            OrderSide::Buy, // Side is unknown for cancel response
665            Decimal::ZERO,
666            None,
667            ccxt_core::types::OrderStatus::Cancelled,
668        ))
669    }
670
671    /// Set leverage for a symbol.
672    pub async fn set_leverage(&self, symbol: &str, leverage: u32, is_cross: bool) -> Result<()> {
673        let market = self.base().market(symbol).await?;
674        let asset_index: u32 = market.id.parse().unwrap_or(0);
675
676        let leverage_type = if is_cross { "cross" } else { "isolated" };
677
678        let action = {
679            let mut map = Map::new();
680            map.insert(
681                "type".to_string(),
682                Value::String("updateLeverage".to_string()),
683            );
684            map.insert("asset".to_string(), asset_index.into());
685            map.insert("isCross".to_string(), is_cross.into());
686            map.insert("leverage".to_string(), leverage.into());
687            Value::Object(map)
688        };
689
690        let nonce = self.get_nonce();
691        let response = self.exchange_request(action, nonce).await?;
692
693        // Check for success
694        if error::is_error_response(&response) {
695            return Err(error::parse_error(&response));
696        }
697
698        info!(
699            "Set leverage for {} to {}x ({})",
700            symbol, leverage, leverage_type
701        );
702
703        Ok(())
704    }
705
706    /// Cancel all orders for a symbol.
707    pub async fn cancel_all_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>> {
708        let _address = self
709            .wallet_address()
710            .ok_or_else(|| Error::authentication("Private key required to cancel orders"))?;
711
712        // First fetch open orders
713        let open_orders = self.fetch_open_orders(symbol, None, None).await?;
714
715        if open_orders.is_empty() {
716            return Ok(Vec::new());
717        }
718
719        // Build cancel requests for all orders
720        let mut cancels = Vec::new();
721        for order in &open_orders {
722            let market = self.base().market(&order.symbol).await?;
723            let asset_index: u32 = market.id.parse().unwrap_or(0);
724            let order_id: u64 = order.id.parse().unwrap_or(0);
725
726            cancels.push(
727                {
728                    let mut map = Map::new();
729                    map.insert("a".to_string(), asset_index.into());
730                    map.insert("o".to_string(), order_id.into());
731                    Ok(Value::Object(map))
732                }
733                .map_err(|e: serde_json::Error| Error::from(ParseError::from(e)))?,
734            );
735        }
736
737        let action = {
738            let mut map = Map::new();
739            map.insert("type".to_string(), Value::String("cancel".to_string()));
740            map.insert("cancels".to_string(), Value::Array(cancels));
741            Value::Object(map)
742        };
743
744        let nonce = self.get_nonce();
745        let _response = self.exchange_request(action, nonce).await?;
746
747        // Return the orders that were canceled
748        let canceled_orders: Vec<Order> = open_orders
749            .into_iter()
750            .map(|mut o| {
751                o.status = ccxt_core::types::OrderStatus::Cancelled;
752                o
753            })
754            .collect();
755
756        info!("Canceled {} orders", canceled_orders.len());
757
758        Ok(canceled_orders)
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765
766    #[test]
767    fn test_get_nonce() {
768        let exchange = HyperLiquid::builder().testnet(true).build().unwrap();
769
770        let nonce1 = exchange.get_nonce();
771        std::thread::sleep(std::time::Duration::from_millis(10));
772        let nonce2 = exchange.get_nonce();
773
774        assert!(nonce2 > nonce1);
775    }
776}