1use crate::{
2 dex_connector::{slippage_price, string_to_decimal, DexConnector},
3 dex_request::{DexError, DexRequest, HttpMethod},
4 dex_websocket::DexWebSocket,
5 BalanceResponse, CreateOrderResponse, FilledOrder, FilledOrdersResponse, OrderSide,
6 TickerResponse,
7};
8use ::serde::{Deserialize, Serialize};
9use async_trait::async_trait;
10use debot_utils::parse_to_decimal;
11use ethers::{signers::LocalWallet, types::H160};
12use futures::{
13 stream::{SplitSink, SplitStream},
14 SinkExt, StreamExt,
15};
16use hyperliquid_rust_sdk_fork::{
17 BaseUrl, ClientCancelRequest, ClientLimit, ClientOrder, ClientOrderRequest, ExchangeClient,
18 ExchangeDataStatus, ExchangeResponseStatus,
19};
20use rust_decimal::prelude::*;
21use rust_decimal::Decimal;
22use std::{
23 collections::HashMap,
24 str::FromStr,
25 sync::{
26 atomic::{AtomicBool, Ordering},
27 Arc,
28 },
29 time::Duration,
30};
31use tokio::signal::unix::SignalKind;
32use tokio::sync::Mutex;
33use tokio::sync::RwLock;
34use tokio::time::sleep;
35use tokio::{net::TcpStream, task::JoinHandle};
36use tokio::{select, signal::unix::signal};
37use tokio_tungstenite::tungstenite::protocol::Message;
38use tokio_tungstenite::MaybeTlsStream;
39use tokio_tungstenite::WebSocketStream;
40
41struct Config {
42 evm_wallet_address: String,
43 symbol_list: Vec<String>,
44}
45
46#[derive(Deserialize, Debug)]
48struct SpotMetaToken {
49 #[serde(rename = "name")]
50 _name: String,
51 #[serde(rename = "szDecimals")]
52 _sz_decimals: u32,
53 #[serde(rename = "weiDecimals")]
54 _wei_decimals: u32,
55 #[serde(rename = "index")]
56 _index: usize,
57}
58
59#[derive(Deserialize, Debug, Clone)]
60struct SpotMetaUniverse {
61 #[serde(rename = "name")]
62 name: String,
63 #[serde(rename = "tokens")]
64 _tokens: Vec<usize>,
65 #[serde(rename = "index")]
66 index: usize,
67}
68
69#[derive(Deserialize, Debug)]
70struct SpotMetaResponse {
71 #[serde(rename = "tokens")]
72 _tokens: Vec<SpotMetaToken>,
73 #[serde(rename = "universe")]
74 universe: Vec<SpotMetaUniverse>,
75}
76
77#[derive(Serialize, Debug)]
78struct InfoRequest<'a> {
79 #[serde(rename = "type")]
80 req_type: &'a str,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 user: Option<&'a str>,
83}
84
85#[derive(Debug)]
86struct TradeResult {
87 pub filled_side: OrderSide,
88 pub filled_size: Decimal,
89 pub filled_value: Decimal,
90 pub filled_fee: Decimal,
91 order_id: String,
92}
93
94#[derive(Default)]
95struct DynamicMarketInfo {
96 pub market_price: Option<Decimal>,
97 pub min_tick: Option<Decimal>,
98 pub volume: Option<Decimal>,
99 pub num_trades: Option<u64>,
100 pub open_interest: Option<Decimal>,
101 pub funding_rate: Option<Decimal>,
102 pub oracle_price: Option<Decimal>,
103}
104
105struct StaticMarketInfo {
106 pub decimals: u32,
107 pub _max_leverage: u32,
108}
109
110pub struct HyperliquidConnector {
111 config: Config,
112 request: DexRequest,
113 web_socket: DexWebSocket,
114 running: Arc<AtomicBool>,
115 read_socket: Arc<Mutex<Option<SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>>>>,
116 write_socket:
117 Arc<Mutex<Option<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>>>,
118 task_handle_read_message: Arc<Mutex<Option<JoinHandle<()>>>>,
119 task_handle_read_sigterm: Arc<Mutex<Option<JoinHandle<()>>>>,
120 trade_results: Arc<RwLock<HashMap<String, HashMap<String, TradeResult>>>>,
122 dynamic_market_info: Arc<RwLock<HashMap<String, DynamicMarketInfo>>>,
124 static_market_info: HashMap<String, StaticMarketInfo>,
125 spot_index_map: HashMap<String, usize>,
126 spot_reverse_map: Arc<HashMap<usize, String>>,
127 exchange_client: ExchangeClient,
128}
129
130#[derive(Debug)]
131struct WebSocketMessage {
132 _channel: String,
133 data: WebSocketData,
134}
135
136#[derive(Debug)]
137enum WebSocketData {
138 AllMidsData(AllMidsData),
139 UserFillsData(UserFillsData),
140 CandleData(CandleData),
141 ActiveAssetCtxData(ActiveAssetCtxData),
142}
143
144#[derive(Deserialize, Debug)]
145struct AllMidsData {
146 mids: HashMap<String, String>,
147}
148
149#[allow(dead_code, non_snake_case)]
150#[derive(Deserialize, Debug)]
151struct CandleData {
152 t: u64, T: u64, s: String, i: String, o: Decimal, c: Decimal, h: Decimal, l: Decimal, v: Decimal, n: u64, }
163
164#[derive(Deserialize, Debug)]
165pub struct ActiveAssetCtxData {
166 pub coin: String, pub ctx: PerpsAssetCtx, }
169
170#[allow(dead_code, non_snake_case)]
171#[derive(Deserialize, Debug)]
172pub struct PerpsAssetCtx {
173 pub dayNtlVlm: Decimal, pub prevDayPx: Decimal, pub markPx: Decimal, pub midPx: Option<Decimal>, pub funding: Decimal, pub openInterest: Decimal, pub oraclePx: Decimal, }
181
182#[derive(Serialize, Deserialize, Debug)]
183pub struct UserFillsData {
184 pub user: String,
185 pub fills: Vec<Fill>,
186}
187
188#[derive(Serialize, Deserialize, Debug)]
189pub struct Fill {
190 pub coin: String,
191 pub px: Decimal,
192 pub sz: Decimal,
193 pub side: String,
194 pub dir: String,
195 #[serde(rename = "closedPnl")]
196 pub closed_pnl: Decimal,
197 pub oid: u64,
198 pub tid: u64,
199 pub fee: Decimal,
200}
201
202impl<'de> Deserialize<'de> for WebSocketMessage {
203 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
204 where
205 D: serde::Deserializer<'de>,
206 {
207 #[derive(Deserialize)]
208 struct Helper {
209 channel: String,
210 data: serde_json::Value,
211 }
212
213 let helper = Helper::deserialize(deserializer)?;
214 let data = match helper.channel.as_str() {
215 "allMids" => {
216 let mids_data = AllMidsData::deserialize(helper.data)
217 .map(WebSocketData::AllMidsData)
218 .map_err(serde::de::Error::custom)?;
219 mids_data
220 }
221 "userFills" => {
222 let fills_data = UserFillsData::deserialize(helper.data)
223 .map(WebSocketData::UserFillsData)
224 .map_err(serde::de::Error::custom)?;
225 fills_data
226 }
227 "candle" => {
228 let candle_data = CandleData::deserialize(helper.data)
229 .map(WebSocketData::CandleData)
230 .map_err(serde::de::Error::custom)?;
231 candle_data
232 }
233 "activeAssetCtx" => {
234 let active_asset_ctx_data = ActiveAssetCtxData::deserialize(helper.data)
235 .map(WebSocketData::ActiveAssetCtxData)
236 .map_err(serde::de::Error::custom)?;
237 active_asset_ctx_data
238 }
239 _ => return Err(serde::de::Error::custom("unknown channel type")),
240 };
241
242 Ok(WebSocketMessage {
243 _channel: helper.channel,
244 data,
245 })
246 }
247}
248
249impl HyperliquidConnector {
250 pub async fn new(
251 rest_endpoint: &str,
252 web_socket_endpoint: &str,
253 private_key: &str,
254 evm_wallet_address: &str,
255 vault_address: Option<String>,
256 use_agent: bool,
257 agent_name: Option<String>,
258 symbol_list: &[&str],
259 ) -> Result<Self, DexError> {
260 let request = DexRequest::new(rest_endpoint.to_owned()).await?;
261 let web_socket = DexWebSocket::new(web_socket_endpoint.to_owned());
262
263 let evm_wallet_address = vault_address
264 .clone()
265 .unwrap_or_else(|| evm_wallet_address.into());
266 let config = Config {
267 evm_wallet_address,
268 symbol_list: symbol_list.iter().map(|s| s.to_string()).collect(),
269 };
270
271 let vault_address: Option<H160> = vault_address
272 .as_deref()
273 .and_then(|v| H160::from_str(v).ok());
274
275 let mut local_wallet: LocalWallet = private_key.parse().unwrap();
276
277 if use_agent {
278 let ec_tmp =
279 ExchangeClient::new(None, local_wallet, Some(BaseUrl::Mainnet), None, None)
280 .await
281 .map_err(|e| DexError::Other(e.to_string()))?;
282
283 let (pk, resp) = ec_tmp
284 .approve_agent(None, agent_name)
285 .await
286 .map_err(|e| DexError::Other(e.to_string()))?;
287 log::info!("Agent approved: {resp:?}");
288
289 local_wallet = pk.parse().unwrap();
290 }
291
292 let exchange_client = ExchangeClient::new(
293 None,
294 local_wallet,
295 Some(BaseUrl::Mainnet),
296 None,
297 vault_address,
298 )
299 .await
300 .map_err(|e| DexError::Other(e.to_string()))?;
301
302 let mut instance = HyperliquidConnector {
303 config,
304 request,
305 web_socket,
306 trade_results: Arc::new(RwLock::new(HashMap::new())),
307 running: Arc::new(AtomicBool::new(false)),
308 read_socket: Arc::new(Mutex::new(None)),
309 write_socket: Arc::new(Mutex::new(None)),
310 task_handle_read_message: Arc::new(Mutex::new(None)),
311 task_handle_read_sigterm: Arc::new(Mutex::new(None)),
312 dynamic_market_info: Arc::new(RwLock::new(HashMap::new())),
313 static_market_info: HashMap::new(),
314 spot_index_map: HashMap::new(),
315 spot_reverse_map: Arc::new(HashMap::new()),
316 exchange_client,
317 };
318
319 instance.retrive_market_metadata().await?;
320
321 let info_payload = serde_json::to_string(&InfoRequest {
322 req_type: "spotMeta",
323 user: None,
324 })
325 .map_err(|e| DexError::Other(e.to_string()))?;
326
327 let spot_meta: SpotMetaResponse = instance
328 .request
329 .handle_request::<SpotMetaResponse, InfoRequest<'_>>(
330 HttpMethod::Post,
331 "/info".into(),
332 &HashMap::new(),
333 info_payload,
334 )
335 .await?;
336
337 let token_name_map: HashMap<usize, String> = spot_meta
339 ._tokens
340 .iter()
341 .map(|t| (t._index, t._name.clone()))
342 .collect();
343
344 let mut idx_from_pair = HashMap::<String, usize>::new();
345 let mut pair_from_idx = HashMap::<usize, String>::new();
346
347 for uni in &spot_meta.universe {
348 let pair = if !uni.name.starts_with('@') {
349 uni.name.clone()
350 } else if uni._tokens.len() == 2 {
351 format!(
352 "{}/{}",
353 token_name_map.get(&uni._tokens[0]).unwrap_or(&"?".into()),
354 token_name_map.get(&uni._tokens[1]).unwrap_or(&"?".into())
355 )
356 } else {
357 log::warn!(
358 "universe idx {} has unexpected token vec {:?}",
359 uni.index,
360 uni._tokens
361 );
362 uni.name.clone()
363 };
364
365 idx_from_pair.insert(pair.clone(), uni.index);
366 pair_from_idx.insert(uni.index, pair);
367 }
368
369 instance.spot_index_map = idx_from_pair;
370 instance.spot_reverse_map = Arc::new(pair_from_idx);
371
372 Ok(instance)
373 }
374
375 pub async fn start_web_socket(&self) -> Result<(), DexError> {
376 log::info!("start_web_socket");
377
378 let (write, read) = self
379 .web_socket
380 .clone()
381 .connect()
382 .await
383 .map_err(|_| DexError::Other("Failed to connect to WebSocket".to_string()))?;
384
385 {
386 let mut read_lock = self.read_socket.lock().await;
387 *read_lock = Some(read);
388 }
389 {
390 let mut write_lock = self.write_socket.lock().await;
391 *write_lock = Some(write);
392 }
393
394 self.running.store(true, Ordering::SeqCst);
395 self.subscribe_to_channels(&self.config.evm_wallet_address)
396 .await?;
397
398 let running = self.running.clone();
399 let read_sock = self.read_socket.clone();
400 let write_sock = self.write_socket.clone();
401 let dmi = self.dynamic_market_info.clone();
402 let trs = self.trade_results.clone();
403 let rev_map = self.spot_reverse_map.clone();
404
405 let reader_handle = tokio::spawn(async move {
406 let mut idle_counter = 0;
407 while running.load(Ordering::SeqCst) {
408 if let Some(stream) = read_sock.lock().await.as_mut() {
409 tokio::select! {
410 msg = stream.next() => match msg {
411 Some(Ok(Message::Text(txt))) => {
412 idle_counter = 0;
413 if txt == "{}" {
414 if let Some(w) = write_sock.lock().await.as_mut() {
415 let _ = w.send(Message::Text(txt)).await;
416 }
417 } else {
418 if let Err(e) = HyperliquidConnector::handle_websocket_message(
419 Message::Text(txt),
420 dmi.clone(),
421 trs.clone(),
422 rev_map.clone(),
423 ).await {
424 log::error!("WebSocket handler error: {:?}", e);
425 break;
426 }
427 }
428 }
429 Some(Ok(_)) => {
430 }
431 Some(Err(err)) => {
432 log::error!("WebSocket read error: {:?}", err);
433 break;
434 }
435 None => {
436 log::info!("WebSocket stream closed");
437 break;
438 }
439 },
440 _ = tokio::time::sleep(Duration::from_secs(10)) => {
441 idle_counter += 1;
442 if idle_counter >= 10 {
443 log::error!("No WebSocket messages for 100s, shutting down reader");
444 break;
445 }
446 }
447 }
448 }
449 }
450 running.store(false, Ordering::SeqCst);
451 log::info!("WebSocket reader task ended");
452 });
453 *self.task_handle_read_message.lock().await = Some(reader_handle);
454
455 let running_for_sig = self.running.clone();
456 let sig_handle = tokio::spawn(async move {
457 let mut sigterm =
458 signal(SignalKind::terminate()).expect("Failed to bind SIGTERM handler");
459 loop {
460 select! {
461 _ = sigterm.recv() => {
462 log::info!("SIGTERM received, stopping WebSocket");
463 running_for_sig.store(false, Ordering::SeqCst);
464 break;
465 }
466 _ = tokio::time::sleep(Duration::from_secs(1)) => {
467 if !running_for_sig.load(Ordering::SeqCst) {
468 break;
469 }
470 }
471 }
472 }
473 });
474 *self.task_handle_read_sigterm.lock().await = Some(sig_handle);
475
476 Ok(())
477 }
478
479 pub async fn stop_web_socket(&self) -> Result<(), DexError> {
480 log::info!("stop_web_socket");
481 self.running.store(false, Ordering::SeqCst);
482
483 {
484 let mut write_guard = self.write_socket.lock().await;
485 if let Some(write_socket) = write_guard.as_mut() {
486 if let Err(e) = write_socket.send(Message::Close(None)).await {
487 log::error!("Failed to send WebSocket close message: {:?}", e);
488 }
489 }
490 *write_guard = None;
491 }
492
493 {
494 let mut read_guard = self.read_socket.lock().await;
495 *read_guard = None;
496 }
497
498 if let Some(handle) = self.task_handle_read_message.lock().await.take() {
499 let _ = handle.await;
500 }
501
502 if let Some(handle) = self.task_handle_read_sigterm.lock().await.take() {
503 let _ = handle.await;
504 }
505
506 drop(self.web_socket.clone());
507
508 Ok(())
509 }
510
511 async fn subscribe_to_channels(&self, user_address: &str) -> Result<(), DexError> {
512 let all_mids_subscription = serde_json::json!({
513 "method": "subscribe",
514 "subscription": {
515 "type": "allMids"
516 }
517 })
518 .to_string();
519
520 let user_fills_subscription = serde_json::json!({
521 "method": "subscribe",
522 "subscription": {
523 "type": "userFills",
524 "user": user_address
525 }
526 })
527 .to_string();
528
529 let mut write_socket_lock = self.write_socket.lock().await;
530
531 if let Some(write_socket) = write_socket_lock.as_mut() {
532 if let Err(e) = write_socket
533 .send(Message::Text(all_mids_subscription))
534 .await
535 {
536 return Err(DexError::WebSocketError(format!(
537 "Failed to subscribe to allMids: {}",
538 e
539 )));
540 }
541
542 if let Err(e) = write_socket
543 .send(Message::Text(user_fills_subscription))
544 .await
545 {
546 return Err(DexError::WebSocketError(format!(
547 "Failed to subscribe to userFills: {}",
548 e
549 )));
550 }
551
552 for symbol in &self.config.symbol_list {
553 let coin = resolve_coin(symbol, &self.spot_index_map);
554 let candle_subscription = serde_json::json!({
555 "method": "subscribe",
556 "subscription": {
557 "type": "candle",
558 "coin": coin,
559 "interval": "1m"
560 }
561 })
562 .to_string();
563
564 if let Err(e) = write_socket.send(Message::Text(candle_subscription)).await {
565 return Err(DexError::WebSocketError(format!(
566 "Failed to subscribe to candle for {}: {}",
567 symbol, e
568 )));
569 }
570
571 let active_asset_ctx_subscription = serde_json::json!({
572 "method": "subscribe",
573 "subscription": {
574 "type": "activeAssetCtx",
575 "coin": coin,
576 }
577 })
578 .to_string();
579
580 if let Err(e) = write_socket
581 .send(Message::Text(active_asset_ctx_subscription))
582 .await
583 {
584 return Err(DexError::WebSocketError(format!(
585 "Failed to subscribe to activeAssetCtx: {}",
586 e
587 )));
588 }
589 }
590 } else {
591 return Err(DexError::WebSocketError(
592 "Write socket is not available".to_string(),
593 ));
594 }
595
596 Ok(())
597 }
598
599 async fn handle_websocket_message(
600 msg: Message,
601 dynamic_market_info: Arc<RwLock<HashMap<String, DynamicMarketInfo>>>,
602 trade_results: Arc<RwLock<HashMap<String, HashMap<String, TradeResult>>>>,
603 spot_reverse_map: Arc<HashMap<usize, String>>,
604 ) -> Result<(), DexError> {
605 if let Message::Text(text) = msg {
606 for line in text.split('\n') {
607 if line.is_empty() {
608 continue;
609 }
610 if let Ok(message) = serde_json::from_str::<WebSocketMessage>(line) {
611 match message.data {
612 WebSocketData::AllMidsData(ref data) => {
613 Self::process_all_mids_message(
614 data,
615 dynamic_market_info.clone(),
616 spot_reverse_map.clone(),
617 )
618 .await;
619 }
620 WebSocketData::CandleData(ref data) => {
621 Self::process_candle_message(
622 data,
623 dynamic_market_info.clone(),
624 spot_reverse_map.clone(),
625 )
626 .await;
627 }
628 WebSocketData::UserFillsData(ref data) => {
629 Self::process_account_data(data, trade_results.clone()).await;
630 }
631 WebSocketData::ActiveAssetCtxData(ref data) => {
632 Self::process_active_asset_ctx_message(
633 data,
634 dynamic_market_info.clone(),
635 spot_reverse_map.clone(),
636 )
637 .await;
638 }
639 }
640 }
641 }
642 }
643 Ok(())
644 }
645
646 async fn process_all_mids_message(
647 mids_data: &AllMidsData,
648 dynamic_market_info: Arc<RwLock<HashMap<String, DynamicMarketInfo>>>,
649 spot_reverse_map: Arc<HashMap<usize, String>>,
650 ) {
651 for (raw_coin, mid_price_str) in &mids_data.mids {
652 let coin = if let Some(stripped) = raw_coin.strip_prefix('@') {
653 stripped
654 .parse::<usize>()
655 .ok()
656 .and_then(|idx| spot_reverse_map.get(&idx).cloned())
657 .unwrap_or_else(|| {
658 log::warn!("spot_reverse_map に {} が無い (@{})", raw_coin, stripped);
659 raw_coin.clone()
660 })
661 } else {
662 raw_coin.clone()
663 };
664
665 let market_key = if coin.contains('/') || coin.contains('-') {
666 coin.clone() } else {
668 format!("{}-USD", coin) };
670
671 if let Ok(mid) = string_to_decimal(Some(mid_price_str.clone())) {
672 let mut guard = dynamic_market_info.write().await;
673 let info = guard.entry(market_key.clone()).or_default();
674 if info.min_tick.is_none() {
675 info.min_tick = Some(Self::calculate_min_tick(mid));
676 }
677 info.market_price = Some(mid);
678
679 if market_key == "UBTC/USDC" {
680 log::info!("mid update UBTC/USDC → {}", mid);
681 }
682 }
683 }
684 }
685
686 async fn process_candle_message(
687 candle: &CandleData,
688 dynamic_market_info: Arc<RwLock<HashMap<String, DynamicMarketInfo>>>,
689 spot_reverse_map: Arc<HashMap<usize, String>>,
690 ) {
691 let coin = if let Some(stripped) = candle.s.strip_prefix('@') {
692 stripped
693 .parse::<usize>()
694 .ok()
695 .and_then(|idx| spot_reverse_map.get(&idx).cloned())
696 .unwrap_or_else(|| {
697 log::warn!(
698 "in spot_reverse_map: {} is missing (@{})",
699 candle.s,
700 stripped
701 );
702 candle.s.clone()
703 })
704 } else {
705 candle.s.clone()
706 };
707
708 let market_key = if coin.contains('/') || coin.contains('-') {
709 coin.clone()
710 } else {
711 format!("{}-USD", coin)
712 };
713
714 let mut guard = dynamic_market_info.write().await;
715 let info = guard.entry(market_key.clone()).or_default();
716 info.volume = Some(candle.v);
717 info.num_trades = Some(candle.n);
718 }
719
720 async fn process_active_asset_ctx_message(
721 asset_data: &ActiveAssetCtxData,
722 dynamic_market_info: Arc<RwLock<HashMap<String, DynamicMarketInfo>>>,
723 spot_reverse_map: Arc<HashMap<usize, String>>,
724 ) {
725 let coin = if let Some(stripped) = asset_data.coin.strip_prefix('@') {
726 stripped
727 .parse::<usize>()
728 .ok()
729 .and_then(|idx| spot_reverse_map.get(&idx).cloned())
730 .unwrap_or_else(|| {
731 log::warn!(
732 "in spot_reverse_map {} is missing (@{})",
733 asset_data.coin,
734 stripped
735 );
736 asset_data.coin.clone()
737 })
738 } else {
739 asset_data.coin.clone()
740 };
741
742 let market_key = if coin.contains('/') || coin.contains('-') {
743 coin.clone()
744 } else {
745 format!("{}-USD", coin)
746 };
747
748 let mut guard = dynamic_market_info.write().await;
749 let info = guard
750 .entry(market_key.clone())
751 .or_insert_with(DynamicMarketInfo::default);
752 info.funding_rate = Some(asset_data.ctx.funding);
753 info.open_interest = Some(asset_data.ctx.openInterest);
754 info.oracle_price = Some(asset_data.ctx.oraclePx);
755 }
756
757 async fn process_account_data(
758 data: &UserFillsData,
759 trade_results: Arc<RwLock<HashMap<String, HashMap<String, TradeResult>>>>,
760 ) {
761 for fill in &data.fills {
762 log::debug!("{:?}", fill);
763
764 let filled_side = if fill.side == "A" {
765 OrderSide::Short
766 } else {
767 OrderSide::Long
768 };
769
770 let filled_size = fill.sz;
771 let filled_price = fill.px;
772 let filled_value = filled_size * filled_price;
773 let filled_fee = fill.fee;
774 let order_id = fill.oid;
775 let trade_id = fill.tid;
776
777 let market_id = if fill.coin.contains('/') || fill.coin.contains('-') {
778 fill.coin.clone()
779 } else {
780 format!("{}-USD", fill.coin)
781 };
782
783 let trade_result = TradeResult {
784 filled_side,
785 filled_size,
786 filled_value,
787 filled_fee,
788 order_id: order_id.to_string(),
789 };
790
791 let mut trade_results_guard = trade_results.write().await;
792 trade_results_guard
793 .entry(market_id.clone())
794 .or_default()
795 .insert(trade_id.to_string(), trade_result);
796 }
797 }
798}
799
800#[derive(Serialize, Debug, Clone)]
801struct HyperliquidDefaultPayload {
802 r#type: String,
803 #[serde(skip_serializing_if = "Option::is_none")]
804 user: Option<String>,
805}
806
807#[derive(Deserialize, Debug)]
808struct HyperliquidRetrieveUserStateResponse {
809 #[serde(rename = "marginSummary")]
810 margin_summary: Option<HyperliquidMarginSummary>,
811}
812#[derive(Deserialize, Debug)]
813struct HyperliquidMarginSummary {
814 #[serde(rename = "accountValue")]
815 account_value: String,
816 #[serde(rename = "totalRawUsd")]
817 total_rawusd: String,
818}
819
820#[derive(Deserialize, Debug)]
821struct HyperliquidRetriveUserOpenOrder {
822 coin: String,
823 oid: u64,
824}
825
826#[derive(Deserialize, Debug)]
827struct HyperliquidRetriveUserPositionResponse {
828 #[serde(rename = "assetPositions")]
829 asset_positions: Vec<HyperliquidRetriveUserPositionResponseBody>,
830}
831#[derive(Deserialize, Debug)]
832struct HyperliquidRetriveUserPositionResponseBody {
833 position: HyperliquidRetriveUserPosition,
834}
835#[derive(Deserialize, Debug)]
836struct HyperliquidRetriveUserPosition {
837 coin: String,
838 szi: Decimal,
839}
840
841#[derive(Deserialize, Debug)]
842struct HyperliquidRetriveMarketMetadataResponse {
843 universe: Vec<HyperliquidRetriveMarketMetadata>,
844}
845#[derive(Deserialize, Debug)]
846struct HyperliquidRetriveMarketMetadata {
847 name: String,
848 #[serde(rename = "szDecimals")]
849 decimals: u32,
850 #[serde(rename = "maxLeverage")]
851 max_leverage: u32,
852}
853
854#[async_trait]
855impl DexConnector for HyperliquidConnector {
856 async fn start(&self) -> Result<(), DexError> {
857 self.start_web_socket().await?;
858 sleep(Duration::from_secs(5)).await;
859 Ok(())
860 }
861
862 async fn stop(&self) -> Result<(), DexError> {
863 self.stop_web_socket().await?;
864 Ok(())
865 }
866
867 async fn restart(&self, max_retries: i32) -> Result<(), DexError> {
868 log::info!("Restarting WebSocket connection...");
869
870 let mut retry_count = 0;
871 let mut backoff_delay = Duration::from_secs(1);
872
873 while retry_count < max_retries {
874 if let Err(e) = self.stop_web_socket().await {
875 log::error!(
876 "Failed to stop WebSocket on attempt {}: {:?}",
877 retry_count + 1,
878 e
879 );
880 } else {
881 log::info!(
882 "Successfully stopped WebSocket on attempt {}.",
883 retry_count + 1
884 );
885 }
886
887 sleep(backoff_delay).await;
888
889 match self.start_web_socket().await {
890 Ok(_) => {
891 log::info!(
892 "Successfully started WebSocket on attempt {}.",
893 retry_count + 1
894 );
895 return Ok(());
896 }
897 Err(e) => {
898 log::error!(
899 "Failed to start WebSocket on attempt {}: {:?}",
900 retry_count + 1,
901 e
902 );
903 retry_count += 1;
904 backoff_delay *= 2; }
906 }
907 }
908
909 log::error!(
910 "Failed to restart WebSocket after {} attempts.",
911 max_retries
912 );
913 Err(DexError::Other(format!(
914 "Failed to restart WebSocket after {} attempts.",
915 max_retries
916 )))
917 }
918
919 async fn set_leverage(&self, symbol: &str, leverage: u32) -> Result<(), DexError> {
920 let asset = Self::extract_asset_name(symbol);
921 self.exchange_client
922 .update_leverage(leverage, asset, false, None)
923 .await
924 .map_err(|e| DexError::Other(e.to_string()))?;
925 Ok(())
926 }
927
928 async fn get_ticker(
929 &self,
930 symbol: &str,
931 _test_price: Option<Decimal>,
932 ) -> Result<TickerResponse, DexError> {
933 if !self.running.load(Ordering::SeqCst) {
934 return Err(DexError::NoConnection);
935 }
936
937 let dynamic_info_guard = self.dynamic_market_info.read().await;
938 let dynamic_info = dynamic_info_guard
939 .get(symbol)
940 .ok_or_else(|| DexError::Other("No dynamic market info available".to_string()))?;
941 let price = dynamic_info
942 .market_price
943 .ok_or_else(|| DexError::Other("No price available".to_string()))?;
944 let min_tick = dynamic_info.min_tick;
945 let volume = dynamic_info.volume;
946 let num_trades = dynamic_info.num_trades;
947 let funding_rate = dynamic_info.funding_rate;
948 let open_interest = dynamic_info.open_interest;
949 let oracle_price = dynamic_info.oracle_price;
950
951 Ok(TickerResponse {
952 symbol: symbol.to_owned(),
953 price,
954 min_tick,
955 min_order: None,
956 volume,
957 num_trades,
958 funding_rate,
959 open_interest,
960 oracle_price,
961 })
962 }
963
964 async fn get_filled_orders(&self, symbol: &str) -> Result<FilledOrdersResponse, DexError> {
965 let mut response: Vec<FilledOrder> = vec![];
966 let trade_results_guard = self.trade_results.read().await;
967 let orders = match trade_results_guard.get(symbol) {
968 Some(v) => v,
969 None => return Ok(FilledOrdersResponse::default()),
970 };
971 for (trade_id, order) in orders.iter() {
972 let filled_order = FilledOrder {
973 order_id: order.order_id.clone(),
974 trade_id: trade_id.clone(),
975 is_rejected: false,
976 filled_side: Some(order.filled_side.clone()),
977 filled_size: Some(order.filled_size),
978 filled_fee: Some(order.filled_fee),
979 filled_value: Some(order.filled_value),
980 };
981 response.push(filled_order);
982 }
983
984 Ok(FilledOrdersResponse { orders: response })
985 }
986
987 async fn get_balance(&self) -> Result<BalanceResponse, DexError> {
988 let request_url = "/info";
989 let action = HyperliquidDefaultPayload {
990 r#type: "clearinghouseState".to_owned(),
991 user: Some(self.config.evm_wallet_address.clone()),
992 };
993 let res = self
994 .handle_request_with_action::<HyperliquidRetrieveUserStateResponse, HyperliquidDefaultPayload>(
995 request_url.to_string(),
996 &action,
997 )
998 .await?;
999
1000 if let Some(summary) = res.margin_summary {
1001 let equity = match parse_to_decimal(&summary.account_value) {
1002 Ok(v) => v,
1003 Err(e) => return Err(DexError::Other(format!("acount_equity: {:?}", e))),
1004 };
1005
1006 let balance = match parse_to_decimal(&summary.total_rawusd) {
1007 Ok(v) => v,
1008 Err(e) => return Err(DexError::Other(format!("balance: {:?}", e))),
1009 };
1010
1011 Ok(BalanceResponse {
1012 equity: equity,
1013 balance: balance,
1014 })
1015 } else {
1016 return Err(DexError::Other(String::from("Unknown error")));
1017 }
1018 }
1019
1020 async fn clear_filled_order(&self, symbol: &str, order_id: &str) -> Result<(), DexError> {
1021 let mut trade_results_guard = self.trade_results.write().await;
1022
1023 if let Some(orders) = trade_results_guard.get_mut(symbol) {
1024 if orders.contains_key(order_id) {
1025 orders.remove(order_id);
1026 } else {
1027 return Err(DexError::Other(format!(
1028 "filled order(order_id:{}({})) does not exist",
1029 order_id, symbol
1030 )));
1031 }
1032 } else {
1033 return Err(DexError::Other(format!(
1034 "filled order(symbol:{}({})) does not exist",
1035 symbol, order_id
1036 )));
1037 }
1038
1039 Ok(())
1040 }
1041
1042 async fn clear_all_filled_order(&self) -> Result<(), DexError> {
1043 let mut trade_results_guard = self.trade_results.write().await;
1044 trade_results_guard.clear();
1045 Ok(())
1046 }
1047
1048 async fn create_order(
1049 &self,
1050 symbol: &str,
1051 size: Decimal,
1052 side: OrderSide,
1053 price: Option<Decimal>,
1054 spread: Option<i64>,
1055 ) -> Result<CreateOrderResponse, DexError> {
1056 let (price, time_in_force) = match price {
1057 Some(v) => (v, "Alo"),
1058 None => {
1059 let price = self.get_worst_price(symbol, &side).await?;
1060 (price, "Ioc")
1061 }
1062 };
1063
1064 let dynamic_market_info_guard = self.dynamic_market_info.read().await;
1065 let market_info = dynamic_market_info_guard
1066 .get(symbol)
1067 .ok_or_else(|| DexError::Other("Market info not found".to_string()))?;
1068 let min_tick = market_info
1069 .min_tick
1070 .ok_or_else(|| DexError::Other("Min tick not set for market".to_string()))?;
1071
1072 let rounded_price = Self::round_price(price, min_tick, side.clone(), spread);
1073 let rounded_size = self.floor_size(size, symbol);
1074
1075 log::debug!("{}, {}({}), {}", symbol, rounded_price, price, rounded_size,);
1076
1077 let asset = resolve_coin(symbol, &self.spot_index_map);
1078
1079 let order = ClientOrderRequest {
1080 asset,
1081 is_buy: side == OrderSide::Long,
1082 reduce_only: false,
1083 limit_px: rounded_price
1084 .to_f64()
1085 .ok_or_else(|| DexError::Other("Conversion to f64 failed".to_string()))?,
1086 sz: rounded_size
1087 .to_f64()
1088 .ok_or_else(|| DexError::Other("Conversion to f64 failed".to_string()))?,
1089 cloid: None,
1090 order_type: ClientOrder::Limit(ClientLimit {
1091 tif: time_in_force.to_string(),
1092 }),
1093 };
1094
1095 let res = self
1096 .exchange_client
1097 .order(order, None)
1098 .await
1099 .map_err(|e| DexError::Other(e.to_string()))?;
1100
1101 let res = match res {
1102 ExchangeResponseStatus::Ok(exchange_response) => exchange_response,
1103 ExchangeResponseStatus::Err(e) => return Err(DexError::ServerResponse(e.to_string())),
1104 };
1105 let status = res.data.unwrap().statuses[0].clone();
1106 let order_id = match status {
1107 ExchangeDataStatus::Filled(order) => order.oid,
1108 ExchangeDataStatus::Resting(order) => order.oid,
1109 _ => {
1110 return Err(DexError::ServerResponse(
1111 "Unknown ExchangeDataStaus".to_owned(),
1112 ))
1113 }
1114 };
1115
1116 Ok(CreateOrderResponse {
1117 order_id: order_id.to_string(),
1118 ordered_price: rounded_price,
1119 ordered_size: rounded_size,
1120 })
1121 }
1122
1123 async fn cancel_order(&self, symbol: &str, order_id: &str) -> Result<(), DexError> {
1124 let asset = resolve_coin(symbol, &self.spot_index_map);
1125 let cancel = ClientCancelRequest {
1126 asset,
1127 oid: u64::from_str(order_id).unwrap_or_default(),
1128 };
1129
1130 self.exchange_client
1131 .cancel(cancel, None)
1132 .await
1133 .map_err(|e| DexError::Other(e.to_string()))?;
1134
1135 Ok(())
1136 }
1137
1138 async fn cancel_all_orders(&self, symbol: Option<String>) -> Result<(), DexError> {
1139 let open_orders = self.get_orders().await?;
1140 let order_ids: Vec<String> = open_orders
1141 .iter()
1142 .filter(|order| {
1143 symbol.as_deref() == Some(&format!("{}-USD", order.coin)) || symbol.is_none()
1144 })
1145 .map(|order| order.oid.to_string())
1146 .collect();
1147
1148 self.cancel_orders(symbol, order_ids).await
1149 }
1150
1151 async fn cancel_orders(
1152 &self,
1153 symbol: Option<String>,
1154 order_ids: Vec<String>,
1155 ) -> Result<(), DexError> {
1156 let open_orders = self.get_orders().await?;
1157
1158 let mut cancels = Vec::new();
1159 for order in open_orders {
1160 let order_symbol = format!("{}-USD", order.coin);
1161 if (symbol.as_deref() == Some(&order_symbol) || symbol.is_none())
1162 && order_ids.contains(&order.oid.to_string())
1163 {
1164 cancels.push(ClientCancelRequest {
1165 asset: Self::extract_asset_name(&order_symbol).to_owned(),
1166 oid: order.oid,
1167 });
1168 }
1169 }
1170
1171 if !cancels.is_empty() {
1172 if let Err(e) = self.exchange_client.bulk_cancel(cancels, None).await {
1173 log::error!("cancel_orders: Failed to cancel orders: {:?}", e);
1174 }
1175 }
1176
1177 Ok(())
1178 }
1179
1180 async fn close_all_positions(&self, symbol: Option<String>) -> Result<(), DexError> {
1181 let open_positions = self.get_positions().await?;
1182
1183 log::warn!("close_all_positions: symbol = {:?}", symbol);
1184
1185 for p in open_positions {
1186 let position = p.position;
1187 let order_symbol = format!("{}-USD", position.coin);
1188 if symbol.as_deref() == Some(&order_symbol) || symbol.is_none() {
1189 let reversed_side = if position.szi.is_sign_negative() {
1190 OrderSide::Long
1191 } else {
1192 OrderSide::Short
1193 };
1194 let size = position.szi.abs();
1195
1196 if let Err(e) = self
1197 .create_order(&order_symbol, size, reversed_side, None, None)
1198 .await
1199 {
1200 log::error!("close_all_positions: {:?}", e);
1201 }
1202 }
1203 }
1204
1205 Ok(())
1206 }
1207
1208 async fn clear_last_trades(&self, _symbol: &str) -> Result<(), DexError> {
1209 Ok(())
1210 }
1211}
1212
1213impl HyperliquidConnector {
1214 async fn handle_request_with_action<T, U>(
1215 &self,
1216 request_url: String,
1217 action: &U,
1218 ) -> Result<T, DexError>
1219 where
1220 T: for<'de> Deserialize<'de>,
1221 U: Serialize + std::fmt::Debug + Clone,
1222 {
1223 let json_payload =
1224 serde_json::to_value(action).map_err(|e| DexError::Other(e.to_string()))?;
1225
1226 log::debug!("json_payload = {:?}", json_payload);
1227
1228 self.request
1229 .handle_request::<T, U>(
1230 HttpMethod::Post,
1231 request_url,
1232 &HashMap::new(),
1233 json_payload.to_string(),
1234 )
1235 .await
1236 .map_err(|e| DexError::Other(e.to_string()))
1237 }
1238
1239 async fn get_positions(
1240 &self,
1241 ) -> Result<Vec<HyperliquidRetriveUserPositionResponseBody>, DexError> {
1242 let request_url = "/info";
1243 let action = HyperliquidDefaultPayload {
1244 r#type: "clearinghouseState".to_owned(),
1245 user: Some(self.config.evm_wallet_address.clone()),
1246 };
1247 let res: HyperliquidRetriveUserPositionResponse = self
1248 .handle_request_with_action::<HyperliquidRetriveUserPositionResponse, HyperliquidDefaultPayload>(
1249 request_url.to_string(),
1250 &action,
1251 )
1252 .await?;
1253
1254 Ok(res.asset_positions)
1255 }
1256
1257 async fn get_orders(&self) -> Result<Vec<HyperliquidRetriveUserOpenOrder>, DexError> {
1258 let request_url = "/info";
1259 let action = HyperliquidDefaultPayload {
1260 r#type: "openOrders".to_owned(),
1261 user: Some(self.config.evm_wallet_address.clone()),
1262 };
1263 let res: Vec<HyperliquidRetriveUserOpenOrder> = self
1264 .handle_request_with_action::<Vec<HyperliquidRetriveUserOpenOrder>, HyperliquidDefaultPayload>(
1265 request_url.to_string(),
1266 &action,
1267 )
1268 .await?;
1269
1270 Ok(res)
1271 }
1272
1273 async fn retrive_market_metadata(&mut self) -> Result<(), DexError> {
1274 let request_url = "/info";
1275 let action = HyperliquidDefaultPayload {
1276 r#type: "meta".to_owned(),
1277 user: None,
1278 };
1279 let res = self
1280 .handle_request_with_action::<HyperliquidRetriveMarketMetadataResponse, HyperliquidDefaultPayload>(
1281 request_url.to_string(),
1282 &action,
1283 )
1284 .await?;
1285
1286 let mut static_market_info_update = HashMap::new();
1287 for metadata in res.universe.into_iter() {
1288 let market_id = format!("{}-USD", metadata.name);
1289 static_market_info_update.insert(
1290 market_id,
1291 StaticMarketInfo {
1292 decimals: metadata.decimals,
1293 _max_leverage: metadata.max_leverage,
1294 },
1295 );
1296 }
1297
1298 self.static_market_info = static_market_info_update;
1299
1300 Ok(())
1301 }
1302
1303 async fn get_worst_price(&self, symbol: &str, side: &OrderSide) -> Result<Decimal, DexError> {
1304 let market_price = self.get_market_price(symbol).await?;
1305
1306 let worst_price = slippage_price(market_price, *side == OrderSide::Long);
1307 Ok(worst_price)
1308 }
1309
1310 async fn get_market_price(&self, symbol: &str) -> Result<Decimal, DexError> {
1311 let market_info_guard = self.dynamic_market_info.read().await;
1312 match market_info_guard.get(symbol) {
1313 Some(v) => match v.market_price {
1314 Some(price) => Ok(price),
1315 None => Err(DexError::Other("Price is None".to_string())),
1316 },
1317 None => Err(DexError::Other("No price available".to_string())),
1318 }
1319 }
1320
1321 fn calculate_min_tick(price: Decimal) -> Decimal {
1322 let price_str = price.to_string();
1323 let parts: Vec<&str> = price_str.split('.').collect();
1324 let integer_part = parts[0];
1325
1326 if integer_part.len() >= 5 {
1327 return Decimal::ONE;
1328 }
1329
1330 let scale = 5 - integer_part.len();
1331
1332 Decimal::new(1, scale as u32)
1333 }
1334
1335 fn round_price(
1336 price: Decimal,
1337 min_tick: Decimal,
1338 order_side: OrderSide,
1339 spread: Option<i64>,
1340 ) -> Decimal {
1341 if min_tick.is_zero() {
1342 log::error!("round_price: min_tick is zero");
1343 return price;
1344 }
1345 let spread = match spread {
1346 Some(v) => Decimal::new(v, 0),
1347 None => Decimal::ZERO,
1348 };
1349
1350 match order_side {
1351 OrderSide::Long => (price / min_tick - spread).floor() * min_tick,
1352 OrderSide::Short => (price / min_tick + spread).ceil() * min_tick,
1353 }
1354 }
1355
1356 fn floor_size(&self, size: Decimal, symbol: &str) -> Decimal {
1357 let decimals = match self.static_market_info.get(symbol) {
1358 Some(v) => v.decimals,
1359 None => {
1360 log::error!("symbol meta is not available: {}", symbol);
1361 return size;
1362 }
1363 };
1364
1365 size.round_dp(decimals)
1366 }
1367
1368 fn extract_asset_name(symbol: &str) -> &str {
1369 symbol.split('-').next().unwrap_or(symbol)
1370 }
1371}
1372
1373fn resolve_coin(sym: &str, map: &HashMap<String, usize>) -> String {
1374 if sym.contains('/') {
1375 match map.get(sym) {
1376 Some(idx) => format!("@{}", idx),
1377 None => {
1378 log::warn!("resolve_coin: {} is not in spot_index_map", sym);
1379 sym.to_string()
1380 }
1381 }
1382 } else {
1383 sym.to_string()
1384 }
1385}