1use 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 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 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 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 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 fn get_nonce(&self) -> u64 {
109 chrono::Utc::now().timestamp_millis() as u64
110 }
111
112 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 let result = self.base().set_markets(markets, None).await?;
138
139 info!("Loaded {} markets for HyperLiquid", result.len());
140 Ok(result)
141 }
142
143 pub async fn load_markets(&self, reload: bool) -> Result<HashMap<String, Arc<Market>>> {
145 let _loading_guard = self.base().market_loading_lock.lock().await;
148
149 {
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 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 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 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 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 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 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 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 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); 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 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 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 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 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 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 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 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 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 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 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 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 Ok(Order::new(
661 id.to_string(),
662 symbol.to_string(),
663 OrderType::Limit,
664 OrderSide::Buy, Decimal::ZERO,
666 None,
667 ccxt_core::types::OrderStatus::Cancelled,
668 ))
669 }
670
671 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 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 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 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 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 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}