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