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