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;
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<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 let markets = self.base().set_markets(markets, None).await?;
138
139 info!("Loaded {} markets for HyperLiquid", markets.len());
140 Ok(markets)
141 }
142
143 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 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 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 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 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 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 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 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 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); 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 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 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 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 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 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 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 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 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 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 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 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 Ok(Order::new(
648 id.to_string(),
649 symbol.to_string(),
650 OrderType::Limit,
651 OrderSide::Buy, Decimal::ZERO,
653 None,
654 ccxt_core::types::OrderStatus::Canceled,
655 ))
656 }
657
658 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 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 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 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 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 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}