dex_connector/
hyperliquid_connector.rs

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// --- Spot metadata support ---
47#[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    // 1st key = symbol, 2nd key = order_id
121    trade_results: Arc<RwLock<HashMap<String, HashMap<String, TradeResult>>>>,
122    // key = symbol
123    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,     // Open time (milliseconds)
153    T: u64,     // Close time (milliseconds)
154    s: String,  // Symbol
155    i: String,  // Interval
156    o: Decimal, // Open price
157    c: Decimal, // Close price
158    h: Decimal, // High price
159    l: Decimal, // Low price
160    v: Decimal, // Volume
161    n: u64,     // Number of trades
162}
163
164#[derive(Deserialize, Debug)]
165pub struct ActiveAssetCtxData {
166    pub coin: String,       // The asset symbol (e.g., BTC-USD)
167    pub ctx: PerpsAssetCtx, // The asset context containing market details
168}
169
170#[allow(dead_code, non_snake_case)]
171#[derive(Deserialize, Debug)]
172pub struct PerpsAssetCtx {
173    pub dayNtlVlm: Decimal,     // Daily notional volume
174    pub prevDayPx: Decimal,     // Previous day's price
175    pub markPx: Decimal,        // Mark price
176    pub midPx: Option<Decimal>, // Mid price (optional)
177    pub funding: Decimal,       // Funding rate
178    pub openInterest: Decimal,  // Open interest
179    pub oraclePx: Decimal,      // Oracle price
180}
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        // index → token_name
338        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() // Spot: UBTC/USDC,  etc.
667            } else {
668                format!("{}-USD", coin) // Perp: BTC-USD, etc.
669            };
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; // Exponential backoff
905                }
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}