1use crate::rate_limiter::{AddressRateLimiter, RestRateLimiter};
2use crate::ws::manager::{
3 managed_stream, HyperliquidSubscription, HyperliquidWsManager, WsSendRateLimiter,
4};
5use crate::ws::{HyperliquidWsBook, HyperliquidWsInboundMessage};
6use async_trait::async_trait;
7use futures_util::{stream, StreamExt};
8use guilder_abstraction::{
9 self, AssetContext, BoxStream, Deposit, EcdsaSignature, ExternalSigner, Fill, FundingPayment,
10 L2Level, L2Snapshot, L2Update, Liquidation, OpenOrder, OrderPlacement, OrderSide, OrderType,
11 OrderUpdate, Position, PredictedFunding, TimeInForce, UserFill, Withdrawal,
12};
13use reqwest::Client;
14use rust_decimal::Decimal;
15use serde::Deserialize;
16use serde_json::Value;
17use std::collections::HashMap;
18use std::str::FromStr;
19use std::sync::{Arc, RwLock};
20const HYPERLIQUID_INFO_URL: &str = "https://api.hyperliquid.xyz/info";
21const HYPERLIQUID_EXCHANGE_URL: &str = "https://api.hyperliquid.xyz/exchange";
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum HyperliquidNetwork {
30 #[default]
32 Mainnet,
33 Testnet,
35}
36
37impl HyperliquidNetwork {
38 pub fn info_url(&self) -> &'static str {
39 match self {
40 Self::Mainnet => HYPERLIQUID_INFO_URL,
41 Self::Testnet => "https://api.hyperliquid-testnet.xyz/info",
42 }
43 }
44 pub fn exchange_url(&self) -> &'static str {
45 match self {
46 Self::Mainnet => HYPERLIQUID_EXCHANGE_URL,
47 Self::Testnet => "https://api.hyperliquid-testnet.xyz/exchange",
48 }
49 }
50 pub fn ws_url(&self) -> &'static str {
51 match self {
52 Self::Mainnet => "wss://api.hyperliquid.xyz/ws",
53 Self::Testnet => "wss://api.hyperliquid-testnet.xyz/ws",
54 }
55 }
56}
57
58async fn parse_response<T: for<'de> serde::Deserialize<'de>>(
59 resp: reqwest::Response,
60) -> Result<T, String> {
61 let status = resp.status();
62 let text = resp
63 .text()
64 .await
65 .map_err(|e| format!("failed to read response body (status {status}): {e}"))?;
66
67 if text.is_empty() {
68 return Err(format!(
69 "empty response body from Hyperliquid (HTTP {status})"
70 ));
71 }
72
73 serde_json::from_str(&text).map_err(|e| {
74 let snippet = if text.len() > 512 {
75 format!("{}... ({} bytes total)", &text[..256], text.len())
76 } else {
77 text.clone()
78 };
79 format!("deserialize error (HTTP {status}): {e}: {snippet}")
80 })
81}
82
83pub struct HyperliquidClient {
84 client: Client,
85 network: HyperliquidNetwork,
86 user_address: Option<String>,
87 private_key: Option<String>,
88 external_signer: Option<Arc<dyn ExternalSigner>>,
89 rest_limiter: Arc<RestRateLimiter>,
90 address_limiter: Arc<AddressRateLimiter>,
91 market_ws_manager: HyperliquidWsManager,
92 user_ws_managers: Arc<RwLock<HashMap<String, HyperliquidWsManager>>>,
93 ws_send_limiter: WsSendRateLimiter,
94}
95
96impl Default for HyperliquidClient {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102impl HyperliquidClient {
103 pub fn new() -> Self {
104 Self::with_network(HyperliquidNetwork::Mainnet)
105 }
106
107 pub fn with_network(network: HyperliquidNetwork) -> Self {
110 let ws_send_limiter = WsSendRateLimiter::new();
111 HyperliquidClient {
112 client: Client::new(),
113 network,
114 user_address: None,
115 private_key: None,
116 external_signer: None,
117 rest_limiter: Arc::new(RestRateLimiter::new()),
118 address_limiter: Arc::new(AddressRateLimiter::new()),
119 market_ws_manager: HyperliquidWsManager::new(None, ws_send_limiter.clone(), network.ws_url()),
120 user_ws_managers: Arc::new(RwLock::new(HashMap::new())),
121 ws_send_limiter,
122 }
123 }
124
125 pub fn with_auth(user_address: impl Into<String>, private_key: String) -> Self {
126 Self::with_network_and_auth(HyperliquidNetwork::Mainnet, user_address, private_key)
127 }
128
129 pub fn with_network_and_auth(
130 network: HyperliquidNetwork,
131 user_address: impl Into<String>,
132 private_key: String,
133 ) -> Self {
134 let ws_send_limiter = WsSendRateLimiter::new();
135 HyperliquidClient {
136 client: Client::new(),
137 network,
138 user_address: Some(user_address.into()),
139 private_key: Some(private_key),
140 external_signer: None,
141 rest_limiter: Arc::new(RestRateLimiter::new()),
142 address_limiter: Arc::new(AddressRateLimiter::new()),
143 market_ws_manager: HyperliquidWsManager::new(None, ws_send_limiter.clone(), network.ws_url()),
144 user_ws_managers: Arc::new(RwLock::new(HashMap::new())),
145 ws_send_limiter,
146 }
147 }
148
149 pub fn with_external_signer(
155 user_address: impl Into<String>,
156 signer: Arc<dyn ExternalSigner>,
157 ) -> Self {
158 let ws_send_limiter = WsSendRateLimiter::new();
159 HyperliquidClient {
160 client: Client::new(),
161 network: HyperliquidNetwork::Mainnet,
162 user_address: Some(user_address.into()),
163 private_key: None,
164 external_signer: Some(signer),
165 rest_limiter: Arc::new(RestRateLimiter::new()),
166 address_limiter: Arc::new(AddressRateLimiter::new()),
167 market_ws_manager: HyperliquidWsManager::new(None, ws_send_limiter.clone(), HyperliquidNetwork::Mainnet.ws_url()),
168 user_ws_managers: Arc::new(RwLock::new(HashMap::new())),
169 ws_send_limiter,
170 }
171 }
172
173 pub fn with_network_and_external_signer(
174 network: HyperliquidNetwork,
175 user_address: impl Into<String>,
176 signer: Arc<dyn ExternalSigner>,
177 ) -> Self {
178 let ws_send_limiter = WsSendRateLimiter::new();
179 HyperliquidClient {
180 client: Client::new(),
181 network,
182 user_address: Some(user_address.into()),
183 private_key: None,
184 external_signer: Some(signer),
185 rest_limiter: Arc::new(RestRateLimiter::new()),
186 address_limiter: Arc::new(AddressRateLimiter::new()),
187 market_ws_manager: HyperliquidWsManager::new(None, ws_send_limiter.clone(), network.ws_url()),
188 user_ws_managers: Arc::new(RwLock::new(HashMap::new())),
189 ws_send_limiter,
190 }
191 }
192
193 pub fn with_budgets(mut self, rest_weight: u32, addr_budget: u64) -> Self {
196 self.rest_limiter = Arc::new(RestRateLimiter::new_with_budget(rest_weight));
197 self.address_limiter = Arc::new(AddressRateLimiter::new_with_budget(addr_budget));
198 self
199 }
200
201 async fn info_post(
205 &self,
206 body: Value,
207 weight: u32,
208 call: &str,
209 ) -> Result<reqwest::Response, String> {
210 self.rest_limiter.acquire(weight).await.map_err(|e| {
211 format!(
212 "rate_limited: info_post ({call}) budget exhausted, retry_after_ms={}",
213 e.retry_after.as_millis()
214 )
215 })?;
216 self.client
217 .post(self.network.info_url())
218 .json(&body)
219 .send()
220 .await
221 .map_err(|e| e.to_string())
222 }
223 fn require_user_address(&self) -> Result<String, String> {
224 self.user_address
225 .clone()
226 .ok_or_else(|| "user address required: use HyperliquidClient::with_auth".to_string())
227 }
228
229 fn require_private_key(&self) -> Result<Option<&str>, String> {
230 if self.external_signer.is_some() {
231 return Ok(None);
232 }
233 self.private_key
234 .as_deref()
235 .map(Some)
236 .ok_or_else(|| "private key or external signer required: use with_auth or with_external_signer".to_string())
237 }
238
239 async fn get_asset_index(&self, symbol: &str) -> Result<usize, String> {
240 let resp = self
242 .info_post(serde_json::json!({"type": "meta"}), 20, "get_asset_index")
243 .await?;
244 let meta: MetaResponse = parse_response(resp).await?;
245 meta.universe
246 .iter()
247 .position(|a| a.name == symbol)
248 .ok_or_else(|| format!("symbol {} not found", symbol))
249 }
250
251 async fn submit_signed_action(
252 &self,
253 action: Value,
254 vault_address: Option<&str>,
255 ) -> Result<Value, String> {
256 let private_key = self.require_private_key()?;
257 let nonce = std::time::SystemTime::now()
258 .duration_since(std::time::UNIX_EPOCH)
259 .unwrap()
260 .as_millis() as u64;
261
262 let (r, s, v) = sign_action(
263 private_key,
264 self.external_signer.as_ref(),
265 &action,
266 vault_address,
267 nonce,
268 )
269 .await?;
270
271 let payload = serde_json::json!({
272 "action": action,
273 "nonce": nonce,
274 "signature": {"r": r, "s": s, "v": v},
275 "vaultAddress": null,
276 "expiresAfter": null
277 });
278
279 self.rest_limiter.acquire(1).await.map_err(|e| {
281 format!(
282 "rate_limited: rest_weight exhausted, retry_after_ms={}",
283 e.retry_after.as_millis()
284 )
285 })?;
286 self.address_limiter.acquire(1, false).await.map_err(|e| {
287 format!(
288 "rate_limited: address quota exhausted, retry_after_ms={}",
289 e.retry_after.as_millis()
290 )
291 })?;
292
293 let resp = self
294 .client
295 .post(self.network.exchange_url())
296 .json(&payload)
297 .send()
298 .await
299 .map_err(|e| e.to_string())?;
300
301 let status = resp.status();
302 if !status.is_success() {
303 let text = resp.text().await.map_err(|e| e.to_string())?;
304 return Err(format!("HTTP {status}: {text}"));
305 }
306
307 let body: Value = parse_response(resp).await?;
308 if body["status"].as_str() == Some("err") {
309 return Err(body["response"]
310 .as_str()
311 .unwrap_or("unknown error")
312 .to_string());
313 }
314 Ok(body)
315 }
316}
317
318#[derive(Deserialize)]
321struct MetaResponse {
322 universe: Vec<AssetInfo>,
323}
324
325#[derive(Deserialize)]
326struct AssetInfo {
327 name: String,
328 #[serde(rename = "szDecimals")]
329 sz_decimals: i32,
330 #[serde(rename = "isDelisted", default)]
331 is_delisted: bool,
332}
333
334type MetaAndAssetCtxsResponse = (MetaResponse, Vec<RestAssetCtx>);
335
336#[derive(Deserialize)]
337#[serde(rename_all = "camelCase")]
338#[allow(dead_code)]
339struct RestAssetCtx {
340 open_interest: String,
341 funding: String,
342 mark_px: String,
343 day_ntl_vlm: String,
344 mid_px: Option<String>,
345 oracle_px: Option<String>,
346 premium: Option<String>,
347 prev_day_px: Option<String>,
348}
349
350#[derive(Deserialize)]
351#[serde(rename_all = "camelCase")]
352#[allow(dead_code)]
353struct ClearinghouseStateResponse {
354 margin_summary: MarginSummary,
355 asset_positions: Vec<AssetPosition>,
356}
357
358#[derive(Deserialize)]
360#[serde(rename_all = "camelCase")]
361#[allow(dead_code)]
362struct MarginSummary {
363 account_value: String,
364 #[serde(default)]
365 total_ntl_pos: Option<String>,
366 #[serde(default)]
367 total_raw_usd: Option<String>,
368 #[serde(default)]
369 total_margin_used: Option<String>,
370}
371
372#[derive(Deserialize)]
373struct AssetPosition {
374 position: PositionDetail,
375}
376
377#[derive(Deserialize)]
378#[serde(rename_all = "camelCase")]
379struct PositionDetail {
380 coin: String,
381 szi: String,
383 entry_px: Option<String>,
384}
385
386#[derive(Deserialize)]
387#[serde(rename_all = "camelCase")]
388struct RestOpenOrder {
389 coin: String,
390 side: String,
391 limit_px: String,
392 sz: String,
393 oid: i64,
394 orig_sz: String,
395 cloid: Option<String>,
396}
397
398type PredictedFundingsResponse = Vec<(String, Vec<(String, Option<PredictedFundingEntry>)>)>;
401
402#[derive(Deserialize)]
403#[serde(rename_all = "camelCase")]
404struct PredictedFundingEntry {
405 funding_rate: String,
406 next_funding_time: i64,
407}
408
409fn parse_decimal(s: &str) -> Option<Decimal> {
412 Decimal::from_str(s).ok()
413}
414
415fn keccak256(data: &[u8]) -> [u8; 32] {
416 use sha3::{Digest, Keccak256};
417 Keccak256::digest(data).into()
418}
419
420fn hyperliquid_domain_separator() -> [u8; 32] {
422 let type_hash = keccak256(
423 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
424 );
425 let name_hash = keccak256(b"Exchange");
426 let version_hash = keccak256(b"1");
427 let mut chain_id = [0u8; 32];
428 chain_id[28..32].copy_from_slice(&1337u32.to_be_bytes());
429 let verifying_contract = [0u8; 32];
430
431 let mut data = [0u8; 160];
432 data[..32].copy_from_slice(&type_hash);
433 data[32..64].copy_from_slice(&name_hash);
434 data[64..96].copy_from_slice(&version_hash);
435 data[96..128].copy_from_slice(&chain_id);
436 data[128..160].copy_from_slice(&verifying_contract);
437 keccak256(&data)
438}
439
440fn value_to_msgpack(val: &Value) -> Vec<u8> {
443 match val {
444 Value::Null => vec![0xc0],
445 Value::Bool(true) => vec![0xc3],
446 Value::Bool(false) => vec![0xc2],
447 Value::Number(n) => {
448 if let Some(i) = n.as_i64() {
449 if i >= 0 {
450 if i <= 127 {
451 vec![i as u8]
452 } else if i <= 255 {
453 vec![0xcc, i as u8]
454 } else if i <= 65535 {
455 let mut buf = vec![0xcd];
456 buf.extend_from_slice(&(i as u16).to_be_bytes());
457 buf
458 } else if i <= 4294967295 {
459 let mut buf = vec![0xce];
460 buf.extend_from_slice(&(i as u32).to_be_bytes());
461 buf
462 } else {
463 let mut buf = vec![0xcf];
464 buf.extend_from_slice(&(i as u64).to_be_bytes());
465 buf
466 }
467 } else if i >= -32 {
468 vec![0xe0 | (i as u8)]
469 } else if i >= -128 {
470 vec![0xd0, i as i8 as u8]
471 } else if i >= -32768 {
472 let mut buf = vec![0xd1];
473 buf.extend_from_slice(&(i as i16).to_be_bytes());
474 buf
475 } else if i >= -2147483648 {
476 let mut buf = vec![0xd2];
477 buf.extend_from_slice(&(i as i32).to_be_bytes());
478 buf
479 } else {
480 let mut buf = vec![0xd3];
481 buf.extend_from_slice(&i.to_be_bytes());
482 buf
483 }
484 } else if let Some(f) = n.as_f64() {
485 let mut buf = vec![0xcb];
486 buf.extend_from_slice(&f.to_be_bytes());
487 buf
488 } else {
489 let u = n.as_u64().unwrap();
490 if u <= 127 {
491 vec![u as u8]
492 } else if u <= 255 {
493 vec![0xcc, u as u8]
494 } else if u <= 65535 {
495 let mut buf = vec![0xcd];
496 buf.extend_from_slice(&(u as u16).to_be_bytes());
497 buf
498 } else if u <= 4294967295 {
499 let mut buf = vec![0xce];
500 buf.extend_from_slice(&(u as u32).to_be_bytes());
501 buf
502 } else {
503 let mut buf = vec![0xcf];
504 buf.extend_from_slice(&u.to_be_bytes());
505 buf
506 }
507 }
508 }
509 Value::String(s) => {
510 let bytes = s.as_bytes();
511 let len = bytes.len();
512 let mut buf = Vec::new();
513 if len <= 31 {
514 buf.push(0xa0 | (len as u8));
515 } else if len <= 255 {
516 buf.push(0xd9);
517 buf.push(len as u8);
518 } else if len <= 65535 {
519 buf.push(0xda);
520 buf.extend_from_slice(&(len as u16).to_be_bytes());
521 } else {
522 buf.push(0xdb);
523 buf.extend_from_slice(&(len as u32).to_be_bytes());
524 }
525 buf.extend_from_slice(bytes);
526 buf
527 }
528 Value::Array(arr) => {
529 let len = arr.len();
530 let mut buf = Vec::new();
531 if len <= 15 {
532 buf.push(0x90 | (len as u8));
533 } else if len <= 65535 {
534 buf.push(0xdc);
535 buf.extend_from_slice(&(len as u16).to_be_bytes());
536 } else {
537 buf.push(0xdd);
538 buf.extend_from_slice(&(len as u32).to_be_bytes());
539 }
540 for item in arr {
541 buf.extend_from_slice(&value_to_msgpack(item));
542 }
543 buf
544 }
545 Value::Object(map) => {
546 let len = map.len();
547 let mut buf = Vec::new();
548 if len <= 15 {
549 buf.push(0x80 | (len as u8));
550 } else if len <= 65535 {
551 buf.push(0xde);
552 buf.extend_from_slice(&(len as u16).to_be_bytes());
553 } else {
554 buf.push(0xdf);
555 buf.extend_from_slice(&(len as u32).to_be_bytes());
556 }
557 for (key, value) in map {
558 buf.extend_from_slice(&value_to_msgpack(&Value::String(key.clone())));
559 buf.extend_from_slice(&value_to_msgpack(value));
560 }
561 buf
562 }
563 }
564}
565
566fn action_to_canonical_msgpack(action: &Value) -> Result<Vec<u8>, String> {
569 Ok(value_to_msgpack(action))
570}
571
572fn build_order_msgpack(
575 asset_idx: usize,
576 is_buy: bool,
577 price: &str,
578 size: &str,
579 reduce_only: bool,
580 order_kind: &str,
581 tif: &[u8],
582 cloid: Option<&str>,
583) -> Vec<u8> {
584 let field_count = if cloid.is_some() { 7 } else { 6 };
585 let mut buf = Vec::new();
586 buf.push(0x80 | (field_count as u8)); buf.extend_from_slice(&value_to_msgpack(&Value::String("a".to_string())));
590 buf.extend_from_slice(&value_to_msgpack(&Value::Number(serde_json::Number::from(
591 asset_idx,
592 ))));
593
594 buf.extend_from_slice(&value_to_msgpack(&Value::String("b".to_string())));
596 buf.push(if is_buy { 0xc3 } else { 0xc2 });
597
598 buf.extend_from_slice(&value_to_msgpack(&Value::String("p".to_string())));
600 buf.extend_from_slice(&value_to_msgpack(&Value::String(price.to_string())));
601
602 buf.extend_from_slice(&value_to_msgpack(&Value::String("s".to_string())));
604 buf.extend_from_slice(&value_to_msgpack(&Value::String(size.to_string())));
605
606 buf.extend_from_slice(&value_to_msgpack(&Value::String("r".to_string())));
608 buf.push(if reduce_only { 0xc3 } else { 0xc2 });
609
610 buf.extend_from_slice(&value_to_msgpack(&Value::String("t".to_string())));
612 buf.push(0x81);
614 buf.extend_from_slice(&value_to_msgpack(&Value::String(order_kind.to_string())));
615 buf.push(0x81);
617 buf.extend_from_slice(&value_to_msgpack(&Value::String("tif".to_string())));
618 buf.extend_from_slice(&value_to_msgpack(&Value::String(
619 String::from_utf8_lossy(tif).to_string(),
620 )));
621
622 if let Some(c) = cloid {
624 buf.extend_from_slice(&value_to_msgpack(&Value::String("c".to_string())));
625 buf.extend_from_slice(&value_to_msgpack(&Value::String(c.to_string())));
626 }
627
628 buf
629}
630
631fn build_trigger_order_msgpack(
634 asset_idx: usize,
635 is_buy: bool,
636 price: &str,
637 size: &str,
638 reduce_only: bool,
639 trigger_px: &str,
640 is_market: bool,
641 tpsl: &str,
642 cloid: Option<&str>,
643) -> Vec<u8> {
644 let field_count = if cloid.is_some() { 7 } else { 6 };
645 let mut buf = Vec::new();
646 buf.push(0x80 | (field_count as u8)); buf.extend_from_slice(&value_to_msgpack(&Value::String("a".to_string())));
650 buf.extend_from_slice(&value_to_msgpack(&Value::Number(serde_json::Number::from(
651 asset_idx,
652 ))));
653
654 buf.extend_from_slice(&value_to_msgpack(&Value::String("b".to_string())));
656 buf.push(if is_buy { 0xc3 } else { 0xc2 });
657
658 buf.extend_from_slice(&value_to_msgpack(&Value::String("p".to_string())));
660 buf.extend_from_slice(&value_to_msgpack(&Value::String(price.to_string())));
661
662 buf.extend_from_slice(&value_to_msgpack(&Value::String("s".to_string())));
664 buf.extend_from_slice(&value_to_msgpack(&Value::String(size.to_string())));
665
666 buf.extend_from_slice(&value_to_msgpack(&Value::String("r".to_string())));
668 buf.push(if reduce_only { 0xc3 } else { 0xc2 });
669
670 buf.extend_from_slice(&value_to_msgpack(&Value::String("t".to_string())));
672 buf.push(0x81); buf.extend_from_slice(&value_to_msgpack(&Value::String("trigger".to_string())));
674 buf.push(0x83);
676 buf.extend_from_slice(&value_to_msgpack(&Value::String("isMarket".to_string())));
677 buf.push(if is_market { 0xc3 } else { 0xc2 });
678 buf.extend_from_slice(&value_to_msgpack(&Value::String("triggerPx".to_string())));
679 buf.extend_from_slice(&value_to_msgpack(&Value::String(trigger_px.to_string())));
680 buf.extend_from_slice(&value_to_msgpack(&Value::String("tpsl".to_string())));
681 buf.extend_from_slice(&value_to_msgpack(&Value::String(tpsl.to_string())));
682
683 if let Some(c) = cloid {
685 buf.extend_from_slice(&value_to_msgpack(&Value::String("c".to_string())));
686 buf.extend_from_slice(&value_to_msgpack(&Value::String(c.to_string())));
687 }
688
689 buf
690}
691
692fn compute_eip712_digest(
697 msgpack: &[u8],
698 nonce: u64,
699 vault_address: Option<&str>,
700) -> Result<[u8; 32], String> {
701 let mut data = msgpack.to_vec();
702 data.extend_from_slice(&nonce.to_be_bytes());
703 match vault_address {
704 None => data.push(0u8),
705 Some(addr) => {
706 data.push(1u8);
707 let addr_bytes = hex::decode(addr.trim_start_matches("0x"))
708 .map_err(|e| format!("invalid vault address: {}", e))?;
709 data.extend_from_slice(&addr_bytes);
710 }
711 }
712
713 let connection_id = keccak256(&data);
714 let agent_type_hash = keccak256(b"Agent(string source,bytes32 connectionId)");
715 let source_hash = keccak256(b"a");
716 let mut struct_data = [0u8; 96];
717 struct_data[..32].copy_from_slice(&agent_type_hash);
718 struct_data[32..64].copy_from_slice(&source_hash);
719 struct_data[64..96].copy_from_slice(&connection_id);
720 let struct_hash = keccak256(&struct_data);
721
722 let domain_sep = hyperliquid_domain_separator();
723 let mut final_data = Vec::with_capacity(66);
724 final_data.extend_from_slice(b"\x19\x01");
725 final_data.extend_from_slice(&domain_sep);
726 final_data.extend_from_slice(&struct_hash);
727 Ok(keccak256(&final_data))
728}
729
730fn format_ecdsa_signature(sig: &EcdsaSignature) -> (String, String, u8) {
732 let r = format!("0x{}", hex::encode(sig.r));
733 let s = format!("0x{}", hex::encode(sig.s));
734 let v = 27u8 + sig.v;
735 (r, s, v)
736}
737
738fn sign_digest_with_key(
740 digest: &[u8; 32],
741 private_key: &str,
742) -> Result<(String, String, u8), String> {
743 use k256::ecdsa::SigningKey;
744
745 let key_bytes = hex::decode(private_key.trim_start_matches("0x"))
746 .map_err(|e| format!("invalid private key: {}", e))?;
747 let signing_key =
748 SigningKey::from_bytes(key_bytes.as_slice().into()).map_err(|e| e.to_string())?;
749 let (sig, recovery_id) = signing_key
750 .sign_prehash_recoverable(digest)
751 .map_err(|e| e.to_string())?;
752
753 let sig_bytes = sig.to_bytes();
754 let r = format!("0x{}", hex::encode(&sig_bytes[..32]));
755 let s = format!("0x{}", hex::encode(&sig_bytes[32..64]));
756 let v = 27u8 + recovery_id.to_byte();
757
758 Ok((r, s, v))
759}
760
761async fn sign_with_msgpack(
764 msgpack: &[u8],
765 private_key: Option<&str>,
766 external_signer: Option<&Arc<dyn ExternalSigner>>,
767 nonce: u64,
768 vault_address: Option<&str>,
769) -> Result<(String, String, u8), String> {
770 let digest = compute_eip712_digest(msgpack, nonce, vault_address)?;
771
772 if let Some(signer) = external_signer {
773 let sig = signer.sign_prehash(&digest).await?;
774 return Ok(format_ecdsa_signature(&sig));
775 }
776
777 let key = private_key.ok_or_else(|| {
778 "no signing method available: provide private_key or external_signer".to_string()
779 })?;
780 sign_digest_with_key(&digest, key)
781}
782
783async fn sign_action(
787 private_key: Option<&str>,
788 external_signer: Option<&Arc<dyn ExternalSigner>>,
789 action: &Value,
790 vault_address: Option<&str>,
791 nonce: u64,
792) -> Result<(String, String, u8), String> {
793 let msgpack_bytes = action_to_canonical_msgpack(action)?;
794 let digest = compute_eip712_digest(&msgpack_bytes, nonce, vault_address)?;
795
796 if let Some(signer) = external_signer {
797 let sig = signer.sign_prehash(&digest).await?;
798 return Ok(format_ecdsa_signature(&sig));
799 }
800
801 let key = private_key.ok_or_else(|| {
802 "no signing method available: provide private_key or external_signer".to_string()
803 })?;
804 sign_digest_with_key(&digest, key)
805}
806
807#[async_trait]
810impl guilder_abstraction::TestServer for HyperliquidClient {
811 async fn ping(&self) -> Result<bool, String> {
813 self.info_post(serde_json::json!({"type": "allMids"}), 2, "ping")
815 .await
816 .map(|r| r.status().is_success())
817 }
818
819 async fn get_server_time(&self) -> Result<i64, String> {
821 Ok(std::time::SystemTime::now()
822 .duration_since(std::time::UNIX_EPOCH)
823 .map(|d| d.as_millis() as i64)
824 .unwrap_or(0))
825 }
826}
827
828#[async_trait]
829impl guilder_abstraction::GetMarketData for HyperliquidClient {
830 async fn get_symbol(&self) -> Result<Vec<String>, String> {
832 let resp = self
834 .info_post(serde_json::json!({"type": "meta"}), 20, "get_symbol")
835 .await?;
836 parse_response::<MetaResponse>(resp)
837 .await
838 .map(|r| r.universe.into_iter().map(|a| a.name).collect())
839 }
840
841 async fn get_open_interest(&self, symbol: String) -> Result<Decimal, String> {
843 let resp = self
845 .info_post(
846 serde_json::json!({"type": "metaAndAssetCtxs"}),
847 20,
848 "get_open_interest",
849 )
850 .await?;
851 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
852 .await?
853 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
854 meta.universe
855 .iter()
856 .position(|a| a.name == symbol)
857 .and_then(|i| ctxs.get(i))
858 .and_then(|ctx| parse_decimal(&ctx.open_interest))
859 .ok_or_else(|| format!("symbol {} not found", symbol))
860 }
861
862 async fn get_asset_context(&self, symbol: String) -> Result<AssetContext, String> {
864 let resp = self
866 .info_post(
867 serde_json::json!({"type": "metaAndAssetCtxs"}),
868 20,
869 "get_asset_context",
870 )
871 .await?;
872 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
873 .await?
874 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
875 let idx = meta
876 .universe
877 .iter()
878 .position(|a| a.name == symbol)
879 .ok_or_else(|| format!("symbol {} not found", symbol))?;
880 let ctx = ctxs
881 .get(idx)
882 .ok_or_else(|| format!("symbol {} not found", symbol))?;
883 Ok(AssetContext {
884 symbol,
885 open_interest: parse_decimal(&ctx.open_interest).ok_or("invalid open_interest")?,
886 funding_rate: parse_decimal(&ctx.funding).ok_or("invalid funding")?,
887 mark_price: parse_decimal(&ctx.mark_px).ok_or("invalid mark_px")?,
888 day_volume: parse_decimal(&ctx.day_ntl_vlm).ok_or("invalid day_ntl_vlm")?,
889 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
890 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
891 premium: ctx.premium.as_deref().and_then(parse_decimal),
892 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
893 sz_decimals: meta.universe.get(idx).map(|a| a.sz_decimals).unwrap_or(0),
894 })
895 }
896
897 async fn get_all_asset_contexts(&self) -> Result<Vec<AssetContext>, String> {
900 let resp = self
902 .info_post(
903 serde_json::json!({"type": "metaAndAssetCtxs"}),
904 20,
905 "get_all_asset_contexts",
906 )
907 .await?;
908 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
909 .await?
910 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
911 let mut result = Vec::with_capacity(meta.universe.len());
912 for (asset, ctx) in meta.universe.iter().zip(ctxs.iter()) {
913 let Some(open_interest) = parse_decimal(&ctx.open_interest) else {
914 continue;
915 };
916 let Some(funding_rate) = parse_decimal(&ctx.funding) else {
917 continue;
918 };
919 let Some(mark_price) = parse_decimal(&ctx.mark_px) else {
920 continue;
921 };
922 let Some(day_volume) = parse_decimal(&ctx.day_ntl_vlm) else {
923 continue;
924 };
925 result.push(AssetContext {
926 symbol: asset.name.clone(),
927 open_interest,
928 funding_rate,
929 mark_price,
930 day_volume,
931 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
932 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
933 premium: ctx.premium.as_deref().and_then(parse_decimal),
934 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
935 sz_decimals: asset.sz_decimals,
936 });
937 }
938 Ok(result)
939 }
940
941 async fn get_sz_decimals(&self, symbol: String) -> Result<i32, String> {
943 let all = self.get_all_sz_decimals().await?;
944 all.get(&symbol)
945 .copied()
946 .ok_or_else(|| format!("symbol {} not found", symbol))
947 }
948
949 async fn get_all_sz_decimals(&self) -> Result<HashMap<String, i32>, String> {
951 let resp = self
953 .info_post(
954 serde_json::json!({"type": "metaAndAssetCtxs"}),
955 20,
956 "get_all_sz_decimals",
957 )
958 .await?;
959 let (meta, _) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
960 .await?
961 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
962 Ok(meta
963 .universe
964 .into_iter()
965 .map(|a| (a.name, a.sz_decimals))
966 .collect())
967 }
968
969 async fn get_l2_orderbook(&self, symbol: String) -> Result<L2Snapshot, String> {
971 let resp = self
973 .info_post(
974 serde_json::json!({"type": "l2Book", "coin": symbol}),
975 2,
976 "get_l2_orderbook",
977 )
978 .await?;
979 let book: Option<HyperliquidWsBook> = parse_response(resp).await?;
980 let book = match book {
981 Some(b) => b,
982 None => {
983 return Ok(L2Snapshot {
984 symbol,
985 bids: vec![],
986 asks: vec![],
987 sequence: 0,
988 })
989 }
990 };
991 let bids = book
992 .levels
993 .first()
994 .into_iter()
995 .flatten()
996 .filter_map(|level| {
997 Some(L2Level {
998 price: parse_decimal(&level.px)?,
999 volume: parse_decimal(&level.sz)?,
1000 })
1001 })
1002 .collect();
1003 let asks = book
1004 .levels
1005 .get(1)
1006 .into_iter()
1007 .flatten()
1008 .filter_map(|level| {
1009 Some(L2Level {
1010 price: parse_decimal(&level.px)?,
1011 volume: parse_decimal(&level.sz)?,
1012 })
1013 })
1014 .collect();
1015 Ok(L2Snapshot {
1016 symbol: book.coin,
1017 bids,
1018 asks,
1019 sequence: book.time,
1020 })
1021 }
1022
1023 async fn get_price(&self, symbol: String) -> Result<Decimal, String> {
1025 let resp = self
1027 .info_post(serde_json::json!({"type": "allMids"}), 2, "get_price")
1028 .await?;
1029 parse_response::<HashMap<String, String>>(resp)
1030 .await?
1031 .get(&symbol)
1032 .and_then(|s| parse_decimal(s))
1033 .ok_or_else(|| format!("symbol {} not found", symbol))
1034 }
1035
1036 async fn get_predicted_fundings(&self) -> Result<Vec<PredictedFunding>, String> {
1039 let resp = self
1041 .info_post(
1042 serde_json::json!({"type": "predictedFundings"}),
1043 20,
1044 "get_predicted_fundings",
1045 )
1046 .await?;
1047 let data: PredictedFundingsResponse = parse_response(resp).await?;
1048 let mut result = Vec::new();
1049 for (symbol, venues) in data {
1050 for (venue, entry) in venues {
1051 let Some(entry) = entry else { continue };
1052 if let Some(funding_rate) = parse_decimal(&entry.funding_rate) {
1053 result.push(PredictedFunding {
1054 symbol: symbol.clone(),
1055 venue,
1056 funding_rate,
1057 next_funding_time_ms: entry.next_funding_time,
1058 });
1059 }
1060 }
1061 }
1062 Ok(result)
1063 }
1064}
1065
1066#[async_trait]
1067impl guilder_abstraction::ManageOrder for HyperliquidClient {
1068 async fn place_order(
1079 &self,
1080 symbol: String,
1081 side: OrderSide,
1082 price: Decimal,
1083 volume: Decimal,
1084 order_type: OrderType,
1085 time_in_force: TimeInForce,
1086 trigger_price: Option<Decimal>,
1087 reduce_only: bool,
1088 cloid: Option<String>,
1089 ) -> Result<OrderPlacement, String> {
1090 let asset_idx = self.get_asset_index(&symbol).await?;
1092 let is_buy = matches!(side, OrderSide::Buy);
1093
1094 let tif_str = match time_in_force {
1095 TimeInForce::Gtc => "Gtc",
1096 TimeInForce::Ioc => "Ioc",
1097 TimeInForce::Fok => "Fok",
1098 TimeInForce::Alo => "Alo",
1099 };
1100
1101 let cloid_hex = cloid.clone();
1102
1103 let is_trigger = matches!(order_type, OrderType::TakeProfit | OrderType::StopLoss);
1105
1106 let (order_msgpack, order_type_json) = if is_trigger {
1107 let trigger_px = trigger_price
1109 .ok_or_else(|| format!("{:?} order requires trigger_price to be set", order_type))?
1110 .normalize()
1111 .to_string();
1112
1113 let tpsl = match order_type {
1114 OrderType::TakeProfit => "tp",
1115 OrderType::StopLoss => "sl",
1116 _ => unreachable!(),
1117 };
1118
1119 let is_market = matches!(time_in_force, TimeInForce::Ioc);
1122
1123 let price_str = if is_market {
1126 trigger_px.clone()
1127 } else {
1128 price.normalize().to_string()
1129 };
1130
1131 let msgpack = build_trigger_order_msgpack(
1132 asset_idx,
1133 is_buy,
1134 &price_str,
1135 &volume.normalize().to_string(),
1136 reduce_only,
1137 &trigger_px,
1138 is_market,
1139 tpsl,
1140 cloid_hex.as_deref(),
1141 );
1142
1143 let json_type = if is_market {
1144 format!(
1145 r#"{{"trigger":{{"isMarket":true,"triggerPx":"{}","tpsl":"{}"}}}}"#,
1146 trigger_px, tpsl
1147 )
1148 } else {
1149 format!(
1150 r#"{{"trigger":{{"isMarket":false,"triggerPx":"{}","tpsl":"{}"}}}}"#,
1151 trigger_px, tpsl
1152 )
1153 };
1154
1155 (msgpack, json_type)
1156 } else {
1157 let (order_kind, tif_bytes) = match order_type {
1159 OrderType::Limit => ("limit", tif_str.as_bytes()),
1160 OrderType::Market => ("limit", b"Ioc".as_slice()),
1161 _ => unreachable!(),
1162 };
1163
1164 let price_str = price.normalize().to_string();
1165 let size_str = volume.normalize().to_string();
1166
1167 let msgpack = build_order_msgpack(
1168 asset_idx,
1169 is_buy,
1170 &price_str,
1171 &size_str,
1172 reduce_only,
1173 order_kind,
1174 tif_bytes,
1175 cloid_hex.as_deref(),
1176 );
1177
1178 let json_type = match order_type {
1179 OrderType::Limit => format!(r#"{{"limit":{{"tif":"{tif_str}"}}}}"#),
1180 OrderType::Market => r#"{"limit":{"tif":"Ioc"}}"#.to_string(),
1181 _ => unreachable!(),
1182 };
1183
1184 (msgpack, json_type)
1185 };
1186
1187 let mut action_msgpack = Vec::new();
1190 action_msgpack.push(0x83); action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("type".to_string())));
1192 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("order".to_string())));
1193 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("orders".to_string())));
1194 action_msgpack.push(0x91); action_msgpack.extend_from_slice(&order_msgpack);
1196 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("grouping".to_string())));
1197 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("na".to_string())));
1198
1199 let cloid_json = if let Some(ref c) = cloid_hex {
1200 format!(r#","c":"{c}""#)
1201 } else {
1202 String::new()
1203 };
1204
1205 let reduce_json = if reduce_only { "true" } else { "false" };
1206
1207 let price_for_json = if is_trigger && matches!(time_in_force, TimeInForce::Ioc) {
1208 if let Some(ref tp) = trigger_price {
1210 tp.normalize().to_string()
1211 } else {
1212 price.normalize().to_string()
1213 }
1214 } else {
1215 price.normalize().to_string()
1216 };
1217
1218 let action_json_str = format!(
1219 r#"{{"type":"order","orders":[{{"a":{asset_idx},"b":{is_buy},"p":"{price}","s":"{size}","r":{reduce_json},"t":{order_type_json}{cloid_json}}}],"grouping":"na"}}"#,
1220 price = price_for_json,
1221 size = volume.normalize().to_string(),
1222 );
1223
1224 let private_key = self.require_private_key()?;
1226 let nonce = std::time::SystemTime::now()
1227 .duration_since(std::time::UNIX_EPOCH)
1228 .unwrap()
1229 .as_millis() as u64;
1230
1231 let (r, s, v) = sign_with_msgpack(
1232 &action_msgpack,
1233 private_key,
1234 self.external_signer.as_ref(),
1235 nonce,
1236 None,
1237 )
1238 .await?;
1239
1240 let payload_str = format!(
1241 r#"{{"action":{},"nonce":{},"signature":{{"r":"{}","s":"{}","v":{}}},"vaultAddress":null,"expiresAfter":null}}"#,
1242 action_json_str, nonce, r, s, v
1243 );
1244
1245 self.rest_limiter.acquire(1).await.map_err(|e| {
1246 format!(
1247 "rate_limited: rest_weight exhausted, retry_after_ms={}",
1248 e.retry_after.as_millis()
1249 )
1250 })?;
1251 self.address_limiter.acquire(1, false).await.map_err(|e| {
1252 format!(
1253 "rate_limited: address quota exhausted, retry_after_ms={}",
1254 e.retry_after.as_millis()
1255 )
1256 })?;
1257
1258 let resp = self
1259 .client
1260 .post(self.network.exchange_url())
1261 .header("Content-Type", "application/json")
1262 .body(payload_str)
1263 .send()
1264 .await
1265 .map_err(|e| e.to_string())?;
1266
1267 let status = resp.status();
1268 if !status.is_success() {
1269 let text = resp.text().await.map_err(|e| e.to_string())?;
1270 return Err(format!("HTTP {status}: {text}"));
1271 }
1272
1273 let body: Value = parse_response(resp).await?;
1274 if body["status"].as_str() == Some("err") {
1275 return Err(body["response"]
1276 .as_str()
1277 .unwrap_or("unknown error")
1278 .to_string());
1279 }
1280 let statuses = &body["response"]["data"]["statuses"][0];
1281
1282 let (oid, returned_cloid, timestamp_ms) = if let Some(resting) = statuses.get("resting") {
1283 let oid = resting["oid"]
1284 .as_i64()
1285 .ok_or_else(|| format!("resting status missing oid: {}", body))?;
1286 let returned_cloid = resting["cloid"].as_str().map(|s: &str| s.to_string());
1287 let ts = std::time::SystemTime::now()
1289 .duration_since(std::time::UNIX_EPOCH)
1290 .unwrap()
1291 .as_millis() as i64;
1292 (oid, returned_cloid, ts)
1293 } else if let Some(filled) = statuses.get("filled") {
1294 let oid = filled["oid"]
1295 .as_i64()
1296 .ok_or_else(|| format!("filled status missing oid: {}", body))?;
1297 let ts = std::time::SystemTime::now()
1299 .duration_since(std::time::UNIX_EPOCH)
1300 .unwrap()
1301 .as_millis() as i64;
1302 (oid, None, ts)
1303 } else if let Some(error) = statuses.get("error") {
1304 return Err(error
1305 .as_str()
1306 .unwrap_or("order rejected with unknown error")
1307 .to_string());
1308 } else {
1309 return Err(format!("unexpected order status: {}", body));
1310 };
1311
1312 Ok(OrderPlacement {
1313 order_id: oid,
1314 symbol,
1315 side,
1316 price,
1317 quantity: volume,
1318 timestamp_ms,
1319 cloid: returned_cloid.or(cloid),
1320 order_type,
1321 trigger_price,
1322 reduce_only,
1323 })
1324 }
1325
1326 async fn change_order_by_cloid(
1329 &self,
1330 cloid: i64,
1331 price: Decimal,
1332 volume: Decimal,
1333 ) -> Result<i64, String> {
1334 let user = self.require_user_address()?;
1335
1336 let resp = self
1338 .info_post(
1339 serde_json::json!({"type": "openOrders", "user": user}),
1340 20,
1341 "change_order_by_cloid",
1342 )
1343 .await?;
1344 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1345 let order = orders
1346 .iter()
1347 .find(|o| o.oid == cloid)
1348 .ok_or_else(|| format!("order {} not found", cloid))?;
1349
1350 let asset_idx = self.get_asset_index(&order.coin).await?;
1351 let is_buy = order.side == "B";
1352
1353 let action = serde_json::json!({
1354 "type": "batchModify",
1355 "modifies": [{
1356 "oid": cloid,
1357 "order": {
1358 "a": asset_idx,
1359 "b": is_buy,
1360 "p": price.to_string(),
1361 "s": volume.to_string(),
1362 "r": false,
1363 "t": {"limit": {"tif": "Gtc"}}
1364 }
1365 }]
1366 });
1367
1368 self.submit_signed_action(action, None).await?;
1369 Ok(cloid)
1370 }
1371
1372 async fn cancel_order_by_cloid(&self, cloid: String) -> Result<(), String> {
1377 let user = self.require_user_address()?;
1378
1379 let resp = self
1381 .info_post(
1382 serde_json::json!({"type": "openOrders", "user": user}),
1383 20,
1384 "cancel_order_by_cloid",
1385 )
1386 .await?;
1387 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1388 let order = orders
1389 .iter()
1390 .find(|o| o.cloid.as_ref() == Some(&cloid))
1391 .ok_or_else(|| format!("order with cloid {} not found", cloid))?;
1392
1393 let meta_resp = self
1395 .info_post(
1396 serde_json::json!({"type": "meta"}),
1397 20,
1398 "cancel_order_by_cloid",
1399 )
1400 .await?;
1401 let meta: MetaResponse = parse_response(meta_resp).await?;
1402
1403 let asset_idx = meta
1404 .universe
1405 .iter()
1406 .position(|a| a.name == order.coin)
1407 .ok_or_else(|| format!("asset {} not found in meta", order.coin))?;
1408
1409 let action = serde_json::json!({
1412 "type": "cancel",
1413 "cancels": [{"a": asset_idx, "o": order.oid}]
1414 });
1415
1416 self.submit_signed_action(action, None).await?;
1417 Ok(())
1418 }
1419
1420 async fn cancel_all_order(&self) -> Result<bool, String> {
1423 let user = self.require_user_address()?;
1424
1425 let resp = self
1427 .info_post(
1428 serde_json::json!({"type": "openOrders", "user": user}),
1429 20,
1430 "cancel_all_order",
1431 )
1432 .await?;
1433 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1434 if orders.is_empty() {
1435 return Ok(true);
1436 }
1437
1438 let meta_resp = self
1440 .info_post(serde_json::json!({"type": "meta"}), 20, "cancel_all_order")
1441 .await?;
1442 let meta: MetaResponse = parse_response(meta_resp).await?;
1443
1444 let cancels: Vec<Value> = orders
1445 .iter()
1446 .filter_map(|o| {
1447 let asset_idx = meta.universe.iter().position(|a| a.name == o.coin)?;
1448 Some(serde_json::json!({"a": asset_idx, "o": o.oid}))
1449 })
1450 .collect();
1451
1452 let action = serde_json::json!({"type": "cancel", "cancels": cancels});
1453 self.submit_signed_action(action, None).await?;
1454 Ok(true)
1455 }
1456}
1457
1458#[async_trait]
1459impl guilder_abstraction::SubscribeMarketData for HyperliquidClient {
1460 fn subscribe_l2_update(&self, symbol: String) -> BoxStream<Result<L2Update, String>> {
1461 Box::pin(stream::iter(vec![Err(format!(
1462 "subscribe_l2_update is unsupported for {symbol}; use subscribe_l2_snapshot"
1463 ))]))
1464 }
1465
1466 fn subscribe_l2_snapshot(&self, symbol: String) -> BoxStream<Result<L2Snapshot, String>> {
1467 let subscription = HyperliquidSubscription::L2Book { coin: symbol };
1468 Box::pin(managed_stream(
1469 self.market_ws_manager.clone(),
1470 subscription,
1471 |msg: HyperliquidWsInboundMessage| {
1472 if let Some(snapshot) = msg.as_l2_snapshot() {
1473 vec![Ok(snapshot)]
1474 } else {
1475 vec![]
1476 }
1477 },
1478 ))
1479 }
1480
1481 fn subscribe_asset_context(&self, symbol: String) -> BoxStream<Result<AssetContext, String>> {
1482 let subscription = HyperliquidSubscription::ActiveAssetCtx { coin: symbol };
1483 Box::pin(managed_stream(
1484 self.market_ws_manager.clone(),
1485 subscription,
1486 |msg: HyperliquidWsInboundMessage| {
1487 if let Some(ctx) = msg.as_asset_context() {
1488 vec![Ok(ctx)]
1489 } else {
1490 vec![]
1491 }
1492 }))
1493 }
1494
1495 fn subscribe_liquidation(&self, user: String) -> BoxStream<Result<Liquidation, String>> {
1496 subscribe_user_stream(
1497 self,
1498 user.clone(),
1499 HyperliquidSubscription::UserEvents { user_addr: user },
1500 |msg: HyperliquidWsInboundMessage| {
1501 if let Some(liq) = msg.as_liquidation() {
1502 vec![Ok(liq)]
1503 } else {
1504 vec![]
1505 }
1506 },
1507 )
1508 }
1509
1510 fn subscribe_fill(&self, symbol: String) -> BoxStream<Result<Fill, String>> {
1511 let subscription = HyperliquidSubscription::Trades { coin: symbol };
1512 Box::pin(managed_stream(
1513 self.market_ws_manager.clone(),
1514 subscription,
1515 |msg: HyperliquidWsInboundMessage| {
1516 if let Some(fills) = msg.as_trades() {
1517 fills.into_iter().map(Ok).collect()
1518 } else {
1519 vec![]
1520 }
1521 }))
1522 }
1523
1524 async fn unsubscribe_all(&self) {
1527 self.market_ws_manager.shutdown();
1528 }
1529}
1530
1531fn subscribe_user_stream<T, F>(
1532 client: &HyperliquidClient,
1533 user_addr: String,
1534 subscription: HyperliquidSubscription,
1535 parse: F,
1536) -> BoxStream<Result<T, String>>
1537where
1538 T: Send + 'static,
1539 F: Fn(HyperliquidWsInboundMessage) -> Vec<Result<T, String>> + Send + Sync + 'static,
1540{
1541 let manager = get_or_create_user_manager(
1542 &client.user_ws_managers,
1543 client.ws_send_limiter.clone(),
1544 user_addr,
1545 client.network.ws_url(),
1546 );
1547 Box::pin(async_stream::stream! {
1548 let stream = managed_stream(manager, subscription, parse);
1549 tokio::pin!(stream);
1550 while let Some(item) = stream.next().await {
1551 yield item;
1552 }
1553 })
1554}
1555
1556fn get_or_create_user_manager(
1557 user_ws_managers: &RwLock<HashMap<String, HyperliquidWsManager>>,
1558 ws_send_limiter: WsSendRateLimiter,
1559 user_addr: String,
1560 ws_url: &'static str,
1561) -> HyperliquidWsManager {
1562 {
1563 let managers = user_ws_managers.read().unwrap_or_else(|e| e.into_inner());
1564 if let Some(manager) = managers.get(&user_addr) {
1565 return manager.clone();
1566 }
1567 }
1568
1569 let mut managers = user_ws_managers.write().unwrap_or_else(|e| e.into_inner());
1570 managers
1571 .entry(user_addr.clone())
1572 .or_insert_with(|| HyperliquidWsManager::new(Some(user_addr), ws_send_limiter, ws_url))
1573 .clone()
1574}
1575
1576#[async_trait]
1577impl guilder_abstraction::GetAccountSnapshot for HyperliquidClient {
1578 async fn get_positions(&self) -> Result<Vec<Position>, String> {
1581 let user = self.require_user_address()?;
1582 let resp = self
1584 .info_post(
1585 serde_json::json!({"type": "clearinghouseState", "user": user}),
1586 2,
1587 "get_positions",
1588 )
1589 .await?;
1590 let state: ClearinghouseStateResponse = parse_response(resp).await?;
1591
1592 Ok(state
1593 .asset_positions
1594 .into_iter()
1595 .filter_map(|ap| {
1596 let p = ap.position;
1597 let size = parse_decimal(&p.szi)?;
1598 if size.is_zero() {
1599 return None;
1600 }
1601 let entry_price = p
1602 .entry_px
1603 .as_deref()
1604 .and_then(parse_decimal)
1605 .unwrap_or_default();
1606 let side = if size > Decimal::ZERO {
1607 OrderSide::Buy
1608 } else {
1609 OrderSide::Sell
1610 };
1611 Some(Position {
1612 symbol: p.coin,
1613 side,
1614 size: size.abs(),
1615 entry_price,
1616 })
1617 })
1618 .collect())
1619 }
1620
1621 async fn get_open_orders(&self) -> Result<Vec<OpenOrder>, String> {
1624 let user = self.require_user_address()?;
1625 let resp = self
1627 .info_post(
1628 serde_json::json!({"type": "openOrders", "user": user}),
1629 20,
1630 "get_open_orders",
1631 )
1632 .await?;
1633 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1634
1635 Ok(orders
1636 .into_iter()
1637 .filter_map(|o| {
1638 let price = parse_decimal(&o.limit_px)?;
1639 let quantity = parse_decimal(&o.orig_sz)?;
1640 let remaining = parse_decimal(&o.sz)?;
1641 let filled_quantity = quantity - remaining;
1642 let side = if o.side == "B" {
1643 OrderSide::Buy
1644 } else {
1645 OrderSide::Sell
1646 };
1647 Some(OpenOrder {
1648 order_id: o.oid,
1649 symbol: o.coin,
1650 side,
1651 price,
1652 quantity,
1653 filled_quantity,
1654 order_type: None, trigger_price: None, reduce_only: false, })
1658 })
1659 .collect())
1660 }
1661
1662 async fn get_balance(&self) -> Result<Vec<guilder_abstraction::AccountBalance>, String> {
1666 let user = self.require_user_address()?;
1667 let resp = self
1669 .info_post(
1670 serde_json::json!({"type": "spotClearinghouseState", "user": user}),
1671 15,
1672 "get_balance",
1673 )
1674 .await?;
1675
1676 #[derive(Deserialize)]
1677 struct SpotStateResponse {
1678 balances: Vec<SpotBalance>,
1679 #[serde(rename = "tokenToAvailableAfterMaintenance")]
1680 token_to_available_after_maintenance: Vec<(i32, String)>,
1681 }
1682
1683 #[allow(dead_code)]
1684 #[derive(Deserialize)]
1685 struct SpotBalance {
1686 coin: String,
1687 total: String,
1688 hold: String,
1689 #[serde(default)]
1690 token: Option<i32>,
1691 #[serde(default)]
1692 #[serde(rename = "entryNtl")]
1693 entry_ntl: Option<String>,
1694 }
1695
1696 let state: SpotStateResponse = parse_response(resp).await?;
1697
1698 let safe_map: HashMap<i32, Decimal> = state
1701 .token_to_available_after_maintenance
1702 .into_iter()
1703 .filter_map(|(token_id, value)| {
1704 parse_decimal(&value).map(|v| (token_id, v))
1705 })
1706 .collect();
1707
1708 let perp_margin_used: Option<Decimal> = match self
1712 .info_post(
1713 serde_json::json!({"type": "clearinghouseState", "user": user}),
1714 2,
1715 "get_balance_margin",
1716 )
1717 .await
1718 {
1719 Ok(resp) => parse_response::<ClearinghouseStateResponse>(resp)
1720 .await
1721 .ok()
1722 .and_then(|ch| {
1723 ch.margin_summary
1724 .total_margin_used
1725 .as_deref()
1726 .and_then(parse_decimal)
1727 }),
1728 Err(_) => None,
1729 };
1730
1731 state
1732 .balances
1733 .into_iter()
1734 .map(|balance| {
1735 let equity = parse_decimal(&balance.total)
1736 .ok_or_else(|| "invalid total balance".to_string())?;
1737 let hold = parse_decimal(&balance.hold)
1738 .ok_or_else(|| "invalid hold balance".to_string())?;
1739 let free = equity - hold;
1740
1741 let safe = balance.token.and_then(|token_id| safe_map.get(&token_id).copied());
1742 let usable = match safe {
1743 Some(s) => std::cmp::min(free, s),
1744 None => free,
1745 };
1746 let maintenance = safe.map(|s| equity - s);
1747
1748 let margin_used = if balance.coin == "USDC" {
1750 perp_margin_used
1751 } else {
1752 None
1753 };
1754
1755 Ok(guilder_abstraction::AccountBalance {
1756 token: balance.coin,
1757 equity,
1758 free,
1759 safe,
1760 usable,
1761 hold,
1762 margin_used,
1763 maintenance,
1764 })
1765 })
1766 .collect()
1767 }
1768
1769 async fn get_user_rate_limit(&self) -> Result<guilder_abstraction::UserRateLimit, String> {
1772 let user = self.require_user_address()?;
1773 let resp = self
1774 .info_post(
1775 serde_json::json!({"type": "userRateLimit", "user": user}),
1776 20,
1777 "get_user_rate_limit",
1778 )
1779 .await?;
1780 let val = parse_response::<Value>(resp).await?;
1781
1782 let cumulative_volume = val["cumVlm"]
1783 .as_str()
1784 .and_then(parse_decimal)
1785 .ok_or_else(|| "missing or invalid cumVlm".to_string())?;
1786 let requests_used = val["nRequestsUsed"]
1787 .as_i64()
1788 .ok_or_else(|| "missing or invalid nRequestsUsed".to_string())?;
1789 let requests_cap = val["nRequestsCap"]
1790 .as_i64()
1791 .ok_or_else(|| "missing or invalid nRequestsCap".to_string())?;
1792 let requests_surplus = val["nRequestsSurplus"]
1793 .as_i64()
1794 .ok_or_else(|| "missing or invalid nRequestsSurplus".to_string())?;
1795
1796 Ok(guilder_abstraction::UserRateLimit {
1797 cumulative_volume,
1798 requests_used,
1799 requests_cap,
1800 requests_surplus,
1801 })
1802 }
1803}
1804
1805#[async_trait]
1806impl guilder_abstraction::SubscribeUserEvents for HyperliquidClient {
1807 fn subscribe_user_fills(&self) -> BoxStream<Result<UserFill, String>> {
1808 let Some(addr) = self.user_address.as_ref() else {
1809 return Box::pin(stream::iter(vec![Err(
1810 "user address not registered".to_string()
1811 )]));
1812 };
1813 subscribe_user_stream(
1814 self,
1815 addr.clone(),
1816 HyperliquidSubscription::UserEvents {
1817 user_addr: addr.clone(),
1818 },
1819 |msg: HyperliquidWsInboundMessage| {
1820 if let Some(fills) = msg.as_user_fills() {
1821 fills.into_iter().map(Ok).collect()
1822 } else {
1823 vec![]
1824 }
1825 },
1826 )
1827 }
1828
1829 fn subscribe_order_updates(&self) -> BoxStream<Result<OrderUpdate, String>> {
1830 let Some(addr) = self.user_address.as_ref() else {
1831 return Box::pin(stream::iter(vec![Err(
1832 "user address not registered".to_string()
1833 )]));
1834 };
1835 subscribe_user_stream(
1836 self,
1837 addr.clone(),
1838 HyperliquidSubscription::OrderUpdates {
1839 user_addr: addr.clone(),
1840 },
1841 |msg: HyperliquidWsInboundMessage| {
1842 if let Some(updates) = msg.as_order_updates() {
1843 updates.into_iter().map(Ok).collect()
1844 } else {
1845 vec![]
1846 }
1847 },
1848 )
1849 }
1850
1851 fn subscribe_funding_payments(&self) -> BoxStream<Result<FundingPayment, String>> {
1852 let Some(addr) = self.user_address.as_ref() else {
1853 return Box::pin(stream::iter(vec![Err(
1854 "user address not registered".to_string()
1855 )]));
1856 };
1857 subscribe_user_stream(
1858 self,
1859 addr.clone(),
1860 HyperliquidSubscription::UserEvents {
1861 user_addr: addr.clone(),
1862 },
1863 |msg: HyperliquidWsInboundMessage| {
1864 if let Some(p) = msg.as_funding_payment() {
1865 vec![Ok(p)]
1866 } else {
1867 vec![]
1868 }
1869 },
1870 )
1871 }
1872
1873 fn subscribe_deposits(&self) -> BoxStream<Result<Deposit, String>> {
1874 let Some(addr) = self.user_address.as_ref() else {
1875 return Box::pin(stream::iter(vec![Err(
1876 "user address not registered".to_string()
1877 )]));
1878 };
1879 subscribe_user_stream(
1880 self,
1881 addr.clone(),
1882 HyperliquidSubscription::NonFundingLedger {
1883 user_addr: addr.clone(),
1884 },
1885 |msg: HyperliquidWsInboundMessage| {
1886 if let Some(deps) = msg.as_deposits() {
1887 deps.into_iter().map(Ok).collect()
1888 } else {
1889 vec![]
1890 }
1891 },
1892 )
1893 }
1894
1895 fn subscribe_withdrawals(&self) -> BoxStream<Result<Withdrawal, String>> {
1896 let Some(addr) = self.user_address.as_ref() else {
1897 return Box::pin(stream::iter(vec![Err(
1898 "user address not registered".to_string()
1899 )]));
1900 };
1901 subscribe_user_stream(
1902 self,
1903 addr.clone(),
1904 HyperliquidSubscription::NonFundingLedger {
1905 user_addr: addr.clone(),
1906 },
1907 |msg: HyperliquidWsInboundMessage| {
1908 if let Some(wds) = msg.as_withdrawals() {
1909 wds.into_iter().map(Ok).collect()
1910 } else {
1911 vec![]
1912 }
1913 },
1914 )
1915 }
1916
1917 fn subscribe_spot_balance(
1919 &self,
1920 ) -> BoxStream<Result<Vec<guilder_abstraction::AccountBalance>, String>> {
1921 let Some(addr) = self.user_address.as_ref() else {
1922 return Box::pin(stream::iter(vec![Err(
1923 "user address not registered".to_string()
1924 )]));
1925 };
1926 self.subscribe_spot_balance_with_address(addr.clone())
1927 }
1928
1929 fn subscribe_spot_balance_with_address(
1931 &self,
1932 address: String,
1933 ) -> BoxStream<Result<Vec<guilder_abstraction::AccountBalance>, String>> {
1934 subscribe_user_stream(
1935 self,
1936 address.clone(),
1937 HyperliquidSubscription::UserEvents { user_addr: address },
1938 |msg: HyperliquidWsInboundMessage| {
1939 if let Some(balances) = msg.as_spot_balance() {
1940 vec![Ok(balances)]
1941 } else {
1942 vec![]
1943 }
1944 },
1945 )
1946 }
1947
1948 async fn unsubscribe_user_events(&self) {
1949 if let Some(addr) = &self.user_address {
1951 self.market_ws_manager.unsubscribe_user(addr);
1952 }
1953 let managers = self.user_ws_managers.read().unwrap_or_else(|e| e.into_inner());
1955 for (addr, manager) in managers.iter() {
1956 manager.unsubscribe_user(addr);
1957 }
1958 }
1959}
1960
1961#[async_trait]
1962impl guilder_abstraction::SubscribeMarketDataOps for HyperliquidClient {
1963 async fn unsubscribe_market_data(&self, symbol: String) {
1964 self.market_ws_manager.unsubscribe_by_coin(&symbol);
1965 }
1966}
1967
1968#[cfg(test)]
1969mod msgpack_tests {
1970 use super::*;
1971 use serde_json::json;
1972
1973 #[test]
1974 fn test_msgpack_null() {
1975 let result = value_to_msgpack(&Value::Null);
1976 assert_eq!(result, vec![0xc0]);
1977 }
1978
1979 #[test]
1980 fn test_msgpack_bool() {
1981 assert_eq!(value_to_msgpack(&Value::Bool(true)), vec![0xc3]);
1982 assert_eq!(value_to_msgpack(&Value::Bool(false)), vec![0xc2]);
1983 }
1984
1985 #[test]
1986 fn test_msgpack_positive_fixint() {
1987 assert_eq!(value_to_msgpack(&json!(0)), vec![0x00]);
1989 assert_eq!(value_to_msgpack(&json!(1)), vec![0x01]);
1990 assert_eq!(value_to_msgpack(&json!(127)), vec![0x7f]);
1991 }
1992
1993 #[test]
1994 fn test_msgpack_uint8() {
1995 assert_eq!(value_to_msgpack(&json!(128)), vec![0xcc, 0x80]);
1997 assert_eq!(value_to_msgpack(&json!(255)), vec![0xcc, 0xff]);
1998 }
1999
2000 #[test]
2001 fn test_msgpack_uint16() {
2002 assert_eq!(value_to_msgpack(&json!(256)), vec![0xcd, 0x01, 0x00]);
2004 assert_eq!(value_to_msgpack(&json!(65535)), vec![0xcd, 0xff, 0xff]);
2005 }
2006
2007 #[test]
2008 fn test_msgpack_uint32() {
2009 assert_eq!(
2011 value_to_msgpack(&json!(65536)),
2012 vec![0xce, 0x00, 0x01, 0x00, 0x00]
2013 );
2014 assert_eq!(
2015 value_to_msgpack(&json!(4294967295u64)),
2016 vec![0xce, 0xff, 0xff, 0xff, 0xff]
2017 );
2018 }
2019
2020 #[test]
2021 fn test_msgpack_uint64() {
2022 let big: u64 = 4294967296;
2024 assert_eq!(
2025 value_to_msgpack(&json!(big)),
2026 vec![0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]
2027 );
2028 }
2029
2030 #[test]
2031 fn test_msgpack_negative_fixint() {
2032 assert_eq!(value_to_msgpack(&json!(-1)), vec![0xff]);
2034 assert_eq!(value_to_msgpack(&json!(-32)), vec![0xe0]);
2035 }
2036
2037 #[test]
2038 fn test_msgpack_int8() {
2039 assert_eq!(value_to_msgpack(&json!(-33)), vec![0xd0, 0xdf]);
2041 assert_eq!(value_to_msgpack(&json!(-128)), vec![0xd0, 0x80]);
2042 }
2043
2044 #[test]
2045 fn test_msgpack_int16() {
2046 assert_eq!(value_to_msgpack(&json!(-129)), vec![0xd1, 0xff, 0x7f]);
2048 assert_eq!(value_to_msgpack(&json!(-32768)), vec![0xd1, 0x80, 0x00]);
2049 }
2050
2051 #[test]
2052 fn test_msgpack_int32() {
2053 assert_eq!(
2055 value_to_msgpack(&json!(-32769)),
2056 vec![0xd2, 0xff, 0xff, 0x7f, 0xff]
2057 );
2058 assert_eq!(
2059 value_to_msgpack(&json!(-2147483648i64)),
2060 vec![0xd2, 0x80, 0x00, 0x00, 0x00]
2061 );
2062 }
2063
2064 #[test]
2065 fn test_msgpack_int64() {
2066 let val: i64 = -2147483649;
2067 let result = value_to_msgpack(&json!(val));
2068 assert_eq!(result[0], 0xd3); assert_eq!(result.len(), 9);
2070 }
2071
2072 #[test]
2073 fn test_msgpack_float() {
2074 let result = value_to_msgpack(&json!(3.14));
2075 assert_eq!(result[0], 0xcb); assert_eq!(result.len(), 9);
2077 }
2078
2079 #[test]
2080 fn test_msgpack_fixstr() {
2081 assert_eq!(value_to_msgpack(&json!("")), vec![0xa0]);
2083 assert_eq!(value_to_msgpack(&json!("hello")), {
2084 let mut expected = vec![0xa5];
2085 expected.extend_from_slice(b"hello");
2086 expected
2087 });
2088 let s = "a".repeat(31);
2089 let result = value_to_msgpack(&json!(s));
2090 assert_eq!(result[0], 0xbf); assert_eq!(result.len(), 32);
2092 }
2093
2094 #[test]
2095 fn test_msgpack_str8() {
2096 let s = "a".repeat(32);
2097 let result = value_to_msgpack(&json!(s));
2098 assert_eq!(result[0], 0xd9); assert_eq!(result[1], 32);
2100 assert_eq!(result.len(), 34);
2101 }
2102
2103 #[test]
2104 fn test_msgpack_fixarray() {
2105 assert_eq!(value_to_msgpack(&json!([])), vec![0x90]);
2107 let result = value_to_msgpack(&json!([1, 2, 3]));
2108 assert_eq!(result[0], 0x93);
2109 assert_eq!(result, vec![0x93, 0x01, 0x02, 0x03]);
2110 }
2111
2112 #[test]
2113 fn test_msgpack_fixmap() {
2114 assert_eq!(value_to_msgpack(&json!({})), vec![0x80]);
2116 let result = value_to_msgpack(&json!({"a": 1}));
2117 assert_eq!(result[0], 0x81); assert_eq!(result, {
2119 let mut expected = vec![0x81];
2120 expected.extend_from_slice(&value_to_msgpack(&json!("a")));
2121 expected.extend_from_slice(&value_to_msgpack(&json!(1)));
2122 expected
2123 });
2124 }
2125
2126 #[test]
2127 fn test_msgmap_preserves_insertion_order() {
2128 let val = json!({
2130 "z": 1,
2131 "a": 2,
2132 "m": 3
2133 });
2134 let result = value_to_msgpack(&val);
2135 assert_eq!(result[0], 0x83);
2137 assert_eq!(result[1], 0xa1); assert_eq!(result[2], b'z');
2140 }
2141
2142 #[test]
2143 fn test_msgpack_mixed_array() {
2144 let val = json!([null, true, false, 42, "hi", [1, 2]]);
2145 let result = value_to_msgpack(&val);
2146 assert_eq!(result[0], 0x96); assert_eq!(result[1], 0xc0); assert_eq!(result[2], 0xc3); assert_eq!(result[3], 0xc2); assert_eq!(result[4], 0x2a); assert_eq!(result[5], 0xa2);
2153 assert_eq!(result[6], b'h');
2154 assert_eq!(result[7], b'i');
2155 }
2156
2157 #[test]
2158 fn test_build_order_msgpack_without_cloid() {
2159 let result = build_order_msgpack(
2160 0, true, "1000", "0.1", false, "limit", b"gtc", None, );
2169 assert_eq!(result[0], 0x86);
2171 }
2172
2173 #[test]
2174 fn test_build_order_msgpack_with_cloid() {
2175 let result = build_order_msgpack(
2176 0, true, "1000", "0.1", false, "limit", b"gtc", Some("my-cloid"), );
2185 assert_eq!(result[0], 0x87);
2187 }
2188
2189 #[test]
2190 fn test_action_to_canonical_msgpack() {
2191 let action = json!({
2192 "type": "order",
2193 "orders": [{"a": 0, "b": true, "p": "1000", "s": "0.1", "r": false, "t": {"limit": {"tif": "gtc"}}}],
2194 "grouping": "na"
2195 });
2196 let result = action_to_canonical_msgpack(&action).unwrap();
2197 assert_eq!(result[0], 0x83);
2199 }
2200
2201 #[test]
2202 fn test_msgpack_matches_rmp_serde_for_simple_values() {
2203 use rmp_serde::to_vec;
2205
2206 for val in [
2207 json!(0),
2208 json!(127),
2209 json!(255),
2210 json!(1000),
2211 json!(-1),
2212 json!(-32),
2213 json!(-128),
2214 ] {
2215 let ours = value_to_msgpack(&val);
2216 let theirs = to_vec(&val).unwrap();
2217 assert_eq!(
2218 ours, theirs,
2219 "mismatch for {}: ours={:?}, rmp={:?}",
2220 val, ours, theirs
2221 );
2222 }
2223 }
2224
2225 #[test]
2226 fn test_msgpack_string_encoding() {
2227 use rmp_serde::to_vec;
2228 for val in [
2229 json!(""),
2230 json!("a"),
2231 json!("hello world"),
2232 json!("BTC-USD"),
2233 ] {
2234 let ours = value_to_msgpack(&val);
2235 let theirs = to_vec(&val).unwrap();
2236 assert_eq!(
2237 ours, theirs,
2238 "mismatch for {}: ours={:?}, rmp={:?}",
2239 val, ours, theirs
2240 );
2241 }
2242 }
2243
2244 #[test]
2245 fn test_msgpack_bool_encoding() {
2246 use rmp_serde::to_vec;
2247 let theirs = to_vec(&json!(true)).unwrap();
2248 assert_eq!(value_to_msgpack(&json!(true)), theirs);
2249 let theirs = to_vec(&json!(false)).unwrap();
2250 assert_eq!(value_to_msgpack(&json!(false)), theirs);
2251 }
2252
2253 #[test]
2254 fn test_msgpack_null_encoding() {
2255 use rmp_serde::to_vec;
2256 let theirs = to_vec(&Value::Null).unwrap();
2257 assert_eq!(value_to_msgpack(&Value::Null), theirs);
2258 }
2259
2260 #[test]
2261 fn test_msgpack_nested_object() {
2262 let val = json!({
2263 "outer": {
2264 "inner": 42
2265 }
2266 });
2267 let result = value_to_msgpack(&val);
2268 assert_eq!(result[0], 0x81); }
2270
2271 #[test]
2272 fn test_msgpack_empty_containers() {
2273 assert_eq!(value_to_msgpack(&json!([])), vec![0x90]);
2274 assert_eq!(value_to_msgpack(&json!({})), vec![0x80]);
2275 }
2276}
2277
2278
2279#[async_trait::async_trait]
2280impl guilder_abstraction::ListingEventSource for HyperliquidClient {
2281 async fn get_listing_events(
2289 &self,
2290 ) -> Result<Vec<guilder_abstraction::ListingEvent>, String> {
2291 let resp = self
2293 .info_post(serde_json::json!({"type": "meta"}), 20, "get_listing_events")
2294 .await?;
2295 let meta = parse_response::<MetaResponse>(resp).await?;
2296 Ok(meta
2297 .universe
2298 .into_iter()
2299 .map(|a| guilder_abstraction::ListingEvent {
2300 ticker: a.name,
2301 exchange: "hyperliquid_perp".to_string(),
2302 event: if a.is_delisted { "delist" } else { "list" }.to_string(),
2303 event_time: String::new(),
2304 })
2305 .collect())
2306 }
2307
2308 async fn get_current_universe(
2309 &self,
2310 ) -> Result<Vec<guilder_abstraction::SymbolStatus>, String> {
2311 let resp = self
2313 .info_post(serde_json::json!({"type": "meta"}), 20, "get_current_universe")
2314 .await?;
2315 let meta = parse_response::<MetaResponse>(resp).await?;
2316 Ok(meta
2317 .universe
2318 .into_iter()
2319 .map(|a| guilder_abstraction::SymbolStatus {
2320 ticker: a.name,
2321 exchange: "hyperliquid_perp".to_string(),
2322 is_delisted: a.is_delisted,
2323 })
2324 .collect())
2325 }
2326}