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