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 pub fn eip712_source(&self) -> &'static str {
60 match self {
61 Self::Mainnet => "a",
62 Self::Testnet => "b",
63 }
64 }
65}
66
67async fn parse_response<T: for<'de> serde::Deserialize<'de>>(
68 resp: reqwest::Response,
69) -> Result<T, String> {
70 let status = resp.status();
71 let text = resp
72 .text()
73 .await
74 .map_err(|e| format!("failed to read response body (status {status}): {e}"))?;
75
76 if text.is_empty() {
77 return Err(format!(
78 "empty response body from Hyperliquid (HTTP {status})"
79 ));
80 }
81
82 serde_json::from_str(&text).map_err(|e| {
83 let snippet = if text.len() > 512 {
84 format!("{}... ({} bytes total)", &text[..256], text.len())
85 } else {
86 text.clone()
87 };
88 format!("deserialize error (HTTP {status}): {e}: {snippet}")
89 })
90}
91
92pub struct HyperliquidClient {
93 client: Client,
94 network: HyperliquidNetwork,
95 user_address: Option<String>,
96 private_key: Option<String>,
97 external_signer: Option<Arc<dyn ExternalSigner>>,
98 rest_limiter: Arc<RestRateLimiter>,
99 address_limiter: Arc<AddressRateLimiter>,
100 market_ws_manager: HyperliquidWsManager,
101 user_ws_managers: Arc<RwLock<HashMap<String, HyperliquidWsManager>>>,
102 ws_send_limiter: WsSendRateLimiter,
103}
104
105impl Default for HyperliquidClient {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl HyperliquidClient {
112 pub fn new() -> Self {
113 Self::with_network(HyperliquidNetwork::Mainnet)
114 }
115
116 pub fn with_network(network: HyperliquidNetwork) -> Self {
119 let ws_send_limiter = WsSendRateLimiter::new();
120 HyperliquidClient {
121 client: Client::new(),
122 network,
123 user_address: None,
124 private_key: None,
125 external_signer: None,
126 rest_limiter: Arc::new(RestRateLimiter::new()),
127 address_limiter: Arc::new(AddressRateLimiter::new()),
128 market_ws_manager: HyperliquidWsManager::new(
129 None,
130 ws_send_limiter.clone(),
131 network.ws_url(),
132 ),
133 user_ws_managers: Arc::new(RwLock::new(HashMap::new())),
134 ws_send_limiter,
135 }
136 }
137
138 pub fn with_auth(user_address: impl Into<String>, private_key: String) -> Self {
139 Self::with_network_and_auth(HyperliquidNetwork::Mainnet, user_address, private_key)
140 }
141
142 pub fn with_network_and_auth(
143 network: HyperliquidNetwork,
144 user_address: impl Into<String>,
145 private_key: String,
146 ) -> Self {
147 let ws_send_limiter = WsSendRateLimiter::new();
148 HyperliquidClient {
149 client: Client::new(),
150 network,
151 user_address: Some(user_address.into()),
152 private_key: Some(private_key),
153 external_signer: None,
154 rest_limiter: Arc::new(RestRateLimiter::new()),
155 address_limiter: Arc::new(AddressRateLimiter::new()),
156 market_ws_manager: HyperliquidWsManager::new(
157 None,
158 ws_send_limiter.clone(),
159 network.ws_url(),
160 ),
161 user_ws_managers: Arc::new(RwLock::new(HashMap::new())),
162 ws_send_limiter,
163 }
164 }
165
166 pub fn with_external_signer(
172 user_address: impl Into<String>,
173 signer: Arc<dyn ExternalSigner>,
174 ) -> Self {
175 let ws_send_limiter = WsSendRateLimiter::new();
176 HyperliquidClient {
177 client: Client::new(),
178 network: HyperliquidNetwork::Mainnet,
179 user_address: Some(user_address.into()),
180 private_key: None,
181 external_signer: Some(signer),
182 rest_limiter: Arc::new(RestRateLimiter::new()),
183 address_limiter: Arc::new(AddressRateLimiter::new()),
184 market_ws_manager: HyperliquidWsManager::new(
185 None,
186 ws_send_limiter.clone(),
187 HyperliquidNetwork::Mainnet.ws_url(),
188 ),
189 user_ws_managers: Arc::new(RwLock::new(HashMap::new())),
190 ws_send_limiter,
191 }
192 }
193
194 pub fn with_network_and_external_signer(
195 network: HyperliquidNetwork,
196 user_address: impl Into<String>,
197 signer: Arc<dyn ExternalSigner>,
198 ) -> Self {
199 let ws_send_limiter = WsSendRateLimiter::new();
200 HyperliquidClient {
201 client: Client::new(),
202 network,
203 user_address: Some(user_address.into()),
204 private_key: None,
205 external_signer: Some(signer),
206 rest_limiter: Arc::new(RestRateLimiter::new()),
207 address_limiter: Arc::new(AddressRateLimiter::new()),
208 market_ws_manager: HyperliquidWsManager::new(
209 None,
210 ws_send_limiter.clone(),
211 network.ws_url(),
212 ),
213 user_ws_managers: Arc::new(RwLock::new(HashMap::new())),
214 ws_send_limiter,
215 }
216 }
217
218 pub fn with_budgets(mut self, rest_weight: u32, addr_budget: u64) -> Self {
221 self.rest_limiter = Arc::new(RestRateLimiter::new_with_budget(rest_weight));
222 self.address_limiter = Arc::new(AddressRateLimiter::new_with_budget(addr_budget));
223 self
224 }
225
226 async fn info_post(
230 &self,
231 body: Value,
232 weight: u32,
233 call: &str,
234 ) -> Result<reqwest::Response, String> {
235 self.rest_limiter.acquire(weight).await.map_err(|e| {
236 format!(
237 "rate_limited: info_post ({call}) budget exhausted, retry_after_ms={}",
238 e.retry_after.as_millis()
239 )
240 })?;
241 self.client
242 .post(self.network.info_url())
243 .json(&body)
244 .send()
245 .await
246 .map_err(|e| e.to_string())
247 }
248 fn require_user_address(&self) -> Result<String, String> {
249 self.user_address
250 .clone()
251 .ok_or_else(|| "user address required: use HyperliquidClient::with_auth".to_string())
252 }
253
254 fn require_private_key(&self) -> Result<Option<&str>, String> {
255 if self.external_signer.is_some() {
256 return Ok(None);
257 }
258 self.private_key.as_deref().map(Some).ok_or_else(|| {
259 "private key or external signer required: use with_auth or with_external_signer"
260 .to_string()
261 })
262 }
263
264 async fn get_asset_index(&self, symbol: &str) -> Result<usize, String> {
265 let resp = self
267 .info_post(serde_json::json!({"type": "meta"}), 20, "get_asset_index")
268 .await?;
269 let meta: MetaResponse = parse_response(resp).await?;
270 meta.universe
271 .iter()
272 .position(|a| a.name == symbol)
273 .ok_or_else(|| format!("symbol {} not found", symbol))
274 }
275
276 async fn submit_signed_action(
277 &self,
278 action: Value,
279 vault_address: Option<&str>,
280 ) -> Result<Value, String> {
281 let private_key = self.require_private_key()?;
282 let nonce = std::time::SystemTime::now()
283 .duration_since(std::time::UNIX_EPOCH)
284 .unwrap()
285 .as_millis() as u64;
286
287 let (r, s, v) = sign_action(
288 private_key,
289 self.external_signer.as_ref(),
290 &action,
291 vault_address,
292 nonce,
293 self.network.eip712_source(),
294 )
295 .await?;
296
297 let payload = serde_json::json!({
298 "action": action,
299 "nonce": nonce,
300 "signature": {"r": r, "s": s, "v": v},
301 "vaultAddress": null,
302 "expiresAfter": null
303 });
304
305 self.rest_limiter.acquire(1).await.map_err(|e| {
307 format!(
308 "rate_limited: rest_weight exhausted, retry_after_ms={}",
309 e.retry_after.as_millis()
310 )
311 })?;
312 self.address_limiter.acquire(1, false).await.map_err(|e| {
313 format!(
314 "rate_limited: address quota exhausted, retry_after_ms={}",
315 e.retry_after.as_millis()
316 )
317 })?;
318
319 let resp = self
320 .client
321 .post(self.network.exchange_url())
322 .json(&payload)
323 .send()
324 .await
325 .map_err(|e| e.to_string())?;
326
327 let status = resp.status();
328 if !status.is_success() {
329 let text = resp.text().await.map_err(|e| e.to_string())?;
330 return Err(format!("HTTP {status}: {text}"));
331 }
332
333 let body: Value = parse_response(resp).await?;
334 if body["status"].as_str() == Some("err") {
335 return Err(body["response"]
336 .as_str()
337 .unwrap_or("unknown error")
338 .to_string());
339 }
340 Ok(body)
341 }
342}
343
344#[derive(Deserialize)]
347struct MetaResponse {
348 universe: Vec<AssetInfo>,
349}
350
351#[derive(Deserialize)]
352struct AssetInfo {
353 name: String,
354 #[serde(rename = "szDecimals")]
355 sz_decimals: i32,
356 #[serde(rename = "isDelisted", default)]
357 is_delisted: bool,
358}
359
360type MetaAndAssetCtxsResponse = (MetaResponse, Vec<RestAssetCtx>);
361
362#[derive(Deserialize)]
363#[serde(rename_all = "camelCase")]
364#[allow(dead_code)]
365struct RestAssetCtx {
366 open_interest: String,
367 funding: String,
368 mark_px: String,
369 day_ntl_vlm: String,
370 mid_px: Option<String>,
371 oracle_px: Option<String>,
372 premium: Option<String>,
373 prev_day_px: Option<String>,
374}
375
376#[derive(Deserialize)]
377#[serde(rename_all = "camelCase")]
378#[allow(dead_code)]
379struct ClearinghouseStateResponse {
380 margin_summary: MarginSummary,
381 asset_positions: Vec<AssetPosition>,
382}
383
384#[derive(Deserialize)]
386#[serde(rename_all = "camelCase")]
387#[allow(dead_code)]
388struct MarginSummary {
389 account_value: String,
390 #[serde(default)]
391 total_ntl_pos: Option<String>,
392 #[serde(default)]
393 total_raw_usd: Option<String>,
394 #[serde(default)]
395 total_margin_used: Option<String>,
396}
397
398#[derive(Deserialize)]
399struct AssetPosition {
400 position: PositionDetail,
401}
402
403#[derive(Deserialize)]
404#[serde(rename_all = "camelCase")]
405struct PositionDetail {
406 coin: String,
407 szi: String,
409 entry_px: Option<String>,
410 #[serde(default)]
412 unrealized_pnl: Option<String>,
413}
414
415#[derive(Deserialize)]
416#[serde(rename_all = "camelCase")]
417struct RestOpenOrder {
418 coin: String,
419 side: String,
420 limit_px: String,
421 sz: String,
422 oid: i64,
423 orig_sz: String,
424 cloid: Option<String>,
425}
426
427type PredictedFundingsResponse = Vec<(String, Vec<(String, Option<PredictedFundingEntry>)>)>;
430
431#[derive(Deserialize)]
432#[serde(rename_all = "camelCase")]
433struct PredictedFundingEntry {
434 funding_rate: String,
435 next_funding_time: i64,
436}
437
438#[derive(Deserialize)]
445pub(crate) struct SpotStateResponse {
446 pub balances: Vec<SpotBalance>,
447 #[serde(default, rename = "tokenToAvailableAfterMaintenance")]
448 pub token_to_available_after_maintenance: Vec<(i32, String)>,
449}
450
451#[derive(Deserialize)]
452pub(crate) struct SpotBalance {
453 pub coin: String,
454 pub total: String,
455 pub hold: String,
456 #[serde(default)]
457 pub token: Option<i32>,
458 #[serde(default)]
459 #[serde(rename = "entryNtl")]
460 pub entry_ntl: Option<String>,
461}
462
463pub(crate) fn map_spot_state(
467 state: SpotStateResponse,
468 perp_margin_used: Option<Decimal>,
469) -> Result<Vec<guilder_abstraction::AccountBalance>, String> {
470 let safe_map: HashMap<i32, Decimal> = state
473 .token_to_available_after_maintenance
474 .into_iter()
475 .filter_map(|(token_id, value)| parse_decimal(&value).map(|v| (token_id, v)))
476 .collect();
477
478 state
479 .balances
480 .into_iter()
481 .map(|balance| {
482 let equity =
483 parse_decimal(&balance.total).ok_or_else(|| "invalid total balance".to_string())?;
484 let hold =
485 parse_decimal(&balance.hold).ok_or_else(|| "invalid hold balance".to_string())?;
486 let free = equity - hold;
487
488 let safe = balance
489 .token
490 .and_then(|token_id| safe_map.get(&token_id).copied());
491 let usable = match safe {
492 Some(s) => std::cmp::min(free, s),
493 None => free,
494 };
495 let maintenance = safe.map(|s| equity - s);
496
497 let margin_used = if balance.coin == "USDC" {
499 perp_margin_used
500 } else {
501 None
502 };
503
504 Ok(guilder_abstraction::AccountBalance {
505 token: balance.coin,
506 equity,
507 free,
508 safe,
509 usable,
510 hold,
511 margin_used,
512 maintenance,
513 settled_usd: None,
514 })
515 })
516 .collect()
517}
518
519pub(crate) fn map_perp_state(
524 state: ClearinghouseStateResponse,
525) -> Result<guilder_abstraction::AccountBalance, String> {
526 let equity = parse_decimal(&state.margin_summary.account_value)
527 .ok_or_else(|| "invalid accountValue".to_string())?;
528 let margin_used = state
529 .margin_summary
530 .total_margin_used
531 .as_deref()
532 .and_then(parse_decimal)
533 .unwrap_or(Decimal::ZERO);
534 let free = equity - margin_used;
535 let venue_uPnL: Decimal = state
540 .asset_positions
541 .iter()
542 .filter_map(|ap| ap.position.unrealized_pnl.as_deref().and_then(parse_decimal))
543 .sum();
544 let settled_usd = Some(equity - venue_uPnL);
545 Ok(guilder_abstraction::AccountBalance {
546 token: crate::PERP_LEDGER_TOKEN.to_string(),
547 equity,
548 free,
549 safe: None,
550 usable: free,
551 hold: Decimal::ZERO,
552 margin_used: Some(margin_used),
553 maintenance: Some(equity - margin_used),
554 settled_usd,
555 })
556}
557
558fn parse_decimal(s: &str) -> Option<Decimal> {
559 Decimal::from_str(s).ok()
560}
561
562fn keccak256(data: &[u8]) -> [u8; 32] {
563 use sha3::{Digest, Keccak256};
564 Keccak256::digest(data).into()
565}
566
567fn hyperliquid_domain_separator() -> [u8; 32] {
569 let type_hash = keccak256(
570 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
571 );
572 let name_hash = keccak256(b"Exchange");
573 let version_hash = keccak256(b"1");
574 let mut chain_id = [0u8; 32];
575 chain_id[28..32].copy_from_slice(&1337u32.to_be_bytes());
576 let verifying_contract = [0u8; 32];
577
578 let mut data = [0u8; 160];
579 data[..32].copy_from_slice(&type_hash);
580 data[32..64].copy_from_slice(&name_hash);
581 data[64..96].copy_from_slice(&version_hash);
582 data[96..128].copy_from_slice(&chain_id);
583 data[128..160].copy_from_slice(&verifying_contract);
584 keccak256(&data)
585}
586
587fn value_to_msgpack(val: &Value) -> Vec<u8> {
590 match val {
591 Value::Null => vec![0xc0],
592 Value::Bool(true) => vec![0xc3],
593 Value::Bool(false) => vec![0xc2],
594 Value::Number(n) => {
595 if let Some(i) = n.as_i64() {
596 if i >= 0 {
597 if i <= 127 {
598 vec![i as u8]
599 } else if i <= 255 {
600 vec![0xcc, i as u8]
601 } else if i <= 65535 {
602 let mut buf = vec![0xcd];
603 buf.extend_from_slice(&(i as u16).to_be_bytes());
604 buf
605 } else if i <= 4294967295 {
606 let mut buf = vec![0xce];
607 buf.extend_from_slice(&(i as u32).to_be_bytes());
608 buf
609 } else {
610 let mut buf = vec![0xcf];
611 buf.extend_from_slice(&(i as u64).to_be_bytes());
612 buf
613 }
614 } else if i >= -32 {
615 vec![0xe0 | (i as u8)]
616 } else if i >= -128 {
617 vec![0xd0, i as i8 as u8]
618 } else if i >= -32768 {
619 let mut buf = vec![0xd1];
620 buf.extend_from_slice(&(i as i16).to_be_bytes());
621 buf
622 } else if i >= -2147483648 {
623 let mut buf = vec![0xd2];
624 buf.extend_from_slice(&(i as i32).to_be_bytes());
625 buf
626 } else {
627 let mut buf = vec![0xd3];
628 buf.extend_from_slice(&i.to_be_bytes());
629 buf
630 }
631 } else if let Some(f) = n.as_f64() {
632 let mut buf = vec![0xcb];
633 buf.extend_from_slice(&f.to_be_bytes());
634 buf
635 } else {
636 let u = n.as_u64().unwrap();
637 if u <= 127 {
638 vec![u as u8]
639 } else if u <= 255 {
640 vec![0xcc, u as u8]
641 } else if u <= 65535 {
642 let mut buf = vec![0xcd];
643 buf.extend_from_slice(&(u as u16).to_be_bytes());
644 buf
645 } else if u <= 4294967295 {
646 let mut buf = vec![0xce];
647 buf.extend_from_slice(&(u as u32).to_be_bytes());
648 buf
649 } else {
650 let mut buf = vec![0xcf];
651 buf.extend_from_slice(&u.to_be_bytes());
652 buf
653 }
654 }
655 }
656 Value::String(s) => {
657 let bytes = s.as_bytes();
658 let len = bytes.len();
659 let mut buf = Vec::new();
660 if len <= 31 {
661 buf.push(0xa0 | (len as u8));
662 } else if len <= 255 {
663 buf.push(0xd9);
664 buf.push(len as u8);
665 } else if len <= 65535 {
666 buf.push(0xda);
667 buf.extend_from_slice(&(len as u16).to_be_bytes());
668 } else {
669 buf.push(0xdb);
670 buf.extend_from_slice(&(len as u32).to_be_bytes());
671 }
672 buf.extend_from_slice(bytes);
673 buf
674 }
675 Value::Array(arr) => {
676 let len = arr.len();
677 let mut buf = Vec::new();
678 if len <= 15 {
679 buf.push(0x90 | (len as u8));
680 } else if len <= 65535 {
681 buf.push(0xdc);
682 buf.extend_from_slice(&(len as u16).to_be_bytes());
683 } else {
684 buf.push(0xdd);
685 buf.extend_from_slice(&(len as u32).to_be_bytes());
686 }
687 for item in arr {
688 buf.extend_from_slice(&value_to_msgpack(item));
689 }
690 buf
691 }
692 Value::Object(map) => {
693 let len = map.len();
694 let mut buf = Vec::new();
695 if len <= 15 {
696 buf.push(0x80 | (len as u8));
697 } else if len <= 65535 {
698 buf.push(0xde);
699 buf.extend_from_slice(&(len as u16).to_be_bytes());
700 } else {
701 buf.push(0xdf);
702 buf.extend_from_slice(&(len as u32).to_be_bytes());
703 }
704 for (key, value) in map {
705 buf.extend_from_slice(&value_to_msgpack(&Value::String(key.clone())));
706 buf.extend_from_slice(&value_to_msgpack(value));
707 }
708 buf
709 }
710 }
711}
712
713fn action_to_canonical_msgpack(action: &Value) -> Result<Vec<u8>, String> {
716 Ok(value_to_msgpack(action))
717}
718
719fn build_order_msgpack(
722 asset_idx: usize,
723 is_buy: bool,
724 price: &str,
725 size: &str,
726 reduce_only: bool,
727 order_kind: &str,
728 tif: &[u8],
729 cloid: Option<&str>,
730) -> Vec<u8> {
731 let field_count = if cloid.is_some() { 7 } else { 6 };
732 let mut buf = Vec::new();
733 buf.push(0x80 | (field_count as u8)); buf.extend_from_slice(&value_to_msgpack(&Value::String("a".to_string())));
737 buf.extend_from_slice(&value_to_msgpack(&Value::Number(serde_json::Number::from(
738 asset_idx,
739 ))));
740
741 buf.extend_from_slice(&value_to_msgpack(&Value::String("b".to_string())));
743 buf.push(if is_buy { 0xc3 } else { 0xc2 });
744
745 buf.extend_from_slice(&value_to_msgpack(&Value::String("p".to_string())));
747 buf.extend_from_slice(&value_to_msgpack(&Value::String(price.to_string())));
748
749 buf.extend_from_slice(&value_to_msgpack(&Value::String("s".to_string())));
751 buf.extend_from_slice(&value_to_msgpack(&Value::String(size.to_string())));
752
753 buf.extend_from_slice(&value_to_msgpack(&Value::String("r".to_string())));
755 buf.push(if reduce_only { 0xc3 } else { 0xc2 });
756
757 buf.extend_from_slice(&value_to_msgpack(&Value::String("t".to_string())));
759 buf.push(0x81);
761 buf.extend_from_slice(&value_to_msgpack(&Value::String(order_kind.to_string())));
762 buf.push(0x81);
764 buf.extend_from_slice(&value_to_msgpack(&Value::String("tif".to_string())));
765 buf.extend_from_slice(&value_to_msgpack(&Value::String(
766 String::from_utf8_lossy(tif).to_string(),
767 )));
768
769 if let Some(c) = cloid {
771 buf.extend_from_slice(&value_to_msgpack(&Value::String("c".to_string())));
772 buf.extend_from_slice(&value_to_msgpack(&Value::String(c.to_string())));
773 }
774
775 buf
776}
777
778fn build_trigger_order_msgpack(
781 asset_idx: usize,
782 is_buy: bool,
783 price: &str,
784 size: &str,
785 reduce_only: bool,
786 trigger_px: &str,
787 is_market: bool,
788 tpsl: &str,
789 cloid: Option<&str>,
790) -> Vec<u8> {
791 let field_count = if cloid.is_some() { 7 } else { 6 };
792 let mut buf = Vec::new();
793 buf.push(0x80 | (field_count as u8)); buf.extend_from_slice(&value_to_msgpack(&Value::String("a".to_string())));
797 buf.extend_from_slice(&value_to_msgpack(&Value::Number(serde_json::Number::from(
798 asset_idx,
799 ))));
800
801 buf.extend_from_slice(&value_to_msgpack(&Value::String("b".to_string())));
803 buf.push(if is_buy { 0xc3 } else { 0xc2 });
804
805 buf.extend_from_slice(&value_to_msgpack(&Value::String("p".to_string())));
807 buf.extend_from_slice(&value_to_msgpack(&Value::String(price.to_string())));
808
809 buf.extend_from_slice(&value_to_msgpack(&Value::String("s".to_string())));
811 buf.extend_from_slice(&value_to_msgpack(&Value::String(size.to_string())));
812
813 buf.extend_from_slice(&value_to_msgpack(&Value::String("r".to_string())));
815 buf.push(if reduce_only { 0xc3 } else { 0xc2 });
816
817 buf.extend_from_slice(&value_to_msgpack(&Value::String("t".to_string())));
819 buf.push(0x81); buf.extend_from_slice(&value_to_msgpack(&Value::String("trigger".to_string())));
821 buf.push(0x83);
823 buf.extend_from_slice(&value_to_msgpack(&Value::String("isMarket".to_string())));
824 buf.push(if is_market { 0xc3 } else { 0xc2 });
825 buf.extend_from_slice(&value_to_msgpack(&Value::String("triggerPx".to_string())));
826 buf.extend_from_slice(&value_to_msgpack(&Value::String(trigger_px.to_string())));
827 buf.extend_from_slice(&value_to_msgpack(&Value::String("tpsl".to_string())));
828 buf.extend_from_slice(&value_to_msgpack(&Value::String(tpsl.to_string())));
829
830 if let Some(c) = cloid {
832 buf.extend_from_slice(&value_to_msgpack(&Value::String("c".to_string())));
833 buf.extend_from_slice(&value_to_msgpack(&Value::String(c.to_string())));
834 }
835
836 buf
837}
838
839fn compute_eip712_digest(
844 msgpack: &[u8],
845 nonce: u64,
846 vault_address: Option<&str>,
847 source: &str,
848) -> Result<[u8; 32], String> {
849 let mut data = msgpack.to_vec();
850 data.extend_from_slice(&nonce.to_be_bytes());
851 match vault_address {
852 None => data.push(0u8),
853 Some(addr) => {
854 data.push(1u8);
855 let addr_bytes = hex::decode(addr.trim_start_matches("0x"))
856 .map_err(|e| format!("invalid vault address: {}", e))?;
857 data.extend_from_slice(&addr_bytes);
858 }
859 }
860
861 let connection_id = keccak256(&data);
862 let agent_type_hash = keccak256(b"Agent(string source,bytes32 connectionId)");
863 let source_hash = keccak256(source.as_bytes());
864 let mut struct_data = [0u8; 96];
865 struct_data[..32].copy_from_slice(&agent_type_hash);
866 struct_data[32..64].copy_from_slice(&source_hash);
867 struct_data[64..96].copy_from_slice(&connection_id);
868 let struct_hash = keccak256(&struct_data);
869
870 let domain_sep = hyperliquid_domain_separator();
871 let mut final_data = Vec::with_capacity(66);
872 final_data.extend_from_slice(b"\x19\x01");
873 final_data.extend_from_slice(&domain_sep);
874 final_data.extend_from_slice(&struct_hash);
875 Ok(keccak256(&final_data))
876}
877
878fn format_ecdsa_signature(sig: &EcdsaSignature) -> (String, String, u8) {
880 let r = format!("0x{}", hex::encode(sig.r));
881 let s = format!("0x{}", hex::encode(sig.s));
882 let v = 27u8 + sig.v;
883 (r, s, v)
884}
885
886fn sign_digest_with_key(
888 digest: &[u8; 32],
889 private_key: &str,
890) -> Result<(String, String, u8), String> {
891 use k256::ecdsa::SigningKey;
892
893 let key_bytes = hex::decode(private_key.trim_start_matches("0x"))
894 .map_err(|e| format!("invalid private key: {}", e))?;
895 let signing_key =
896 SigningKey::from_bytes(key_bytes.as_slice().into()).map_err(|e| e.to_string())?;
897 let (sig, recovery_id) = signing_key
898 .sign_prehash_recoverable(digest)
899 .map_err(|e| e.to_string())?;
900
901 let sig_bytes = sig.to_bytes();
902 let r = format!("0x{}", hex::encode(&sig_bytes[..32]));
903 let s = format!("0x{}", hex::encode(&sig_bytes[32..64]));
904 let v = 27u8 + recovery_id.to_byte();
905
906 Ok((r, s, v))
907}
908
909async fn sign_with_msgpack(
912 msgpack: &[u8],
913 private_key: Option<&str>,
914 external_signer: Option<&Arc<dyn ExternalSigner>>,
915 nonce: u64,
916 vault_address: Option<&str>,
917 source: &str,
918) -> Result<(String, String, u8), String> {
919 let digest = compute_eip712_digest(msgpack, nonce, vault_address, source)?;
920
921 if let Some(signer) = external_signer {
922 let sig = signer.sign_prehash(&digest).await?;
923 return Ok(format_ecdsa_signature(&sig));
924 }
925
926 let key = private_key.ok_or_else(|| {
927 "no signing method available: provide private_key or external_signer".to_string()
928 })?;
929 sign_digest_with_key(&digest, key)
930}
931
932async fn sign_action(
936 private_key: Option<&str>,
937 external_signer: Option<&Arc<dyn ExternalSigner>>,
938 action: &Value,
939 vault_address: Option<&str>,
940 nonce: u64,
941 source: &str,
942) -> Result<(String, String, u8), String> {
943 let msgpack_bytes = action_to_canonical_msgpack(action)?;
944 let digest = compute_eip712_digest(&msgpack_bytes, nonce, vault_address, source)?;
945
946 if let Some(signer) = external_signer {
947 let sig = signer.sign_prehash(&digest).await?;
948 return Ok(format_ecdsa_signature(&sig));
949 }
950
951 let key = private_key.ok_or_else(|| {
952 "no signing method available: provide private_key or external_signer".to_string()
953 })?;
954 sign_digest_with_key(&digest, key)
955}
956
957#[async_trait]
960impl guilder_abstraction::TestServer for HyperliquidClient {
961 async fn ping(&self) -> Result<bool, String> {
963 self.info_post(serde_json::json!({"type": "allMids"}), 2, "ping")
965 .await
966 .map(|r| r.status().is_success())
967 }
968
969 async fn get_server_time(&self) -> Result<i64, String> {
971 Ok(std::time::SystemTime::now()
972 .duration_since(std::time::UNIX_EPOCH)
973 .map(|d| d.as_millis() as i64)
974 .unwrap_or(0))
975 }
976}
977
978#[async_trait]
979impl guilder_abstraction::GetMarketData for HyperliquidClient {
980 async fn get_symbol(&self) -> Result<Vec<String>, String> {
982 let resp = self
984 .info_post(serde_json::json!({"type": "meta"}), 20, "get_symbol")
985 .await?;
986 parse_response::<MetaResponse>(resp)
987 .await
988 .map(|r| r.universe.into_iter().map(|a| a.name).collect())
989 }
990
991 async fn get_open_interest(&self, symbol: String) -> Result<Decimal, String> {
993 let resp = self
995 .info_post(
996 serde_json::json!({"type": "metaAndAssetCtxs"}),
997 20,
998 "get_open_interest",
999 )
1000 .await?;
1001 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
1002 .await?
1003 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
1004 meta.universe
1005 .iter()
1006 .position(|a| a.name == symbol)
1007 .and_then(|i| ctxs.get(i))
1008 .and_then(|ctx| parse_decimal(&ctx.open_interest))
1009 .ok_or_else(|| format!("symbol {} not found", symbol))
1010 }
1011
1012 async fn get_asset_context(&self, symbol: String) -> Result<AssetContext, String> {
1014 let resp = self
1016 .info_post(
1017 serde_json::json!({"type": "metaAndAssetCtxs"}),
1018 20,
1019 "get_asset_context",
1020 )
1021 .await?;
1022 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
1023 .await?
1024 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
1025 let idx = meta
1026 .universe
1027 .iter()
1028 .position(|a| a.name == symbol)
1029 .ok_or_else(|| format!("symbol {} not found", symbol))?;
1030 let ctx = ctxs
1031 .get(idx)
1032 .ok_or_else(|| format!("symbol {} not found", symbol))?;
1033 Ok(AssetContext {
1034 symbol,
1035 open_interest: parse_decimal(&ctx.open_interest).ok_or("invalid open_interest")?,
1036 funding_rate: parse_decimal(&ctx.funding).ok_or("invalid funding")?,
1037 mark_price: parse_decimal(&ctx.mark_px).ok_or("invalid mark_px")?,
1038 day_volume: parse_decimal(&ctx.day_ntl_vlm).ok_or("invalid day_ntl_vlm")?,
1039 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
1040 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
1041 premium: ctx.premium.as_deref().and_then(parse_decimal),
1042 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
1043 sz_decimals: meta.universe.get(idx).map(|a| a.sz_decimals).unwrap_or(0),
1044 })
1045 }
1046
1047 async fn get_all_asset_contexts(&self) -> Result<Vec<AssetContext>, String> {
1050 let resp = self
1052 .info_post(
1053 serde_json::json!({"type": "metaAndAssetCtxs"}),
1054 20,
1055 "get_all_asset_contexts",
1056 )
1057 .await?;
1058 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
1059 .await?
1060 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
1061 let mut result = Vec::with_capacity(meta.universe.len());
1062 for (asset, ctx) in meta.universe.iter().zip(ctxs.iter()) {
1063 let Some(open_interest) = parse_decimal(&ctx.open_interest) else {
1064 continue;
1065 };
1066 let Some(funding_rate) = parse_decimal(&ctx.funding) else {
1067 continue;
1068 };
1069 let Some(mark_price) = parse_decimal(&ctx.mark_px) else {
1070 continue;
1071 };
1072 let Some(day_volume) = parse_decimal(&ctx.day_ntl_vlm) else {
1073 continue;
1074 };
1075 result.push(AssetContext {
1076 symbol: asset.name.clone(),
1077 open_interest,
1078 funding_rate,
1079 mark_price,
1080 day_volume,
1081 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
1082 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
1083 premium: ctx.premium.as_deref().and_then(parse_decimal),
1084 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
1085 sz_decimals: asset.sz_decimals,
1086 });
1087 }
1088 Ok(result)
1089 }
1090
1091 async fn get_sz_decimals(&self, symbol: String) -> Result<i32, String> {
1093 let all = self.get_all_sz_decimals().await?;
1094 all.get(&symbol)
1095 .copied()
1096 .ok_or_else(|| format!("symbol {} not found", symbol))
1097 }
1098
1099 async fn get_all_sz_decimals(&self) -> Result<HashMap<String, i32>, String> {
1101 let resp = self
1103 .info_post(
1104 serde_json::json!({"type": "metaAndAssetCtxs"}),
1105 20,
1106 "get_all_sz_decimals",
1107 )
1108 .await?;
1109 let (meta, _) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
1110 .await?
1111 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
1112 Ok(meta
1113 .universe
1114 .into_iter()
1115 .map(|a| (a.name, a.sz_decimals))
1116 .collect())
1117 }
1118
1119 async fn get_l2_orderbook(&self, symbol: String) -> Result<L2Snapshot, String> {
1121 let resp = self
1123 .info_post(
1124 serde_json::json!({"type": "l2Book", "coin": symbol}),
1125 2,
1126 "get_l2_orderbook",
1127 )
1128 .await?;
1129 let book: Option<HyperliquidWsBook> = parse_response(resp).await?;
1130 let book = match book {
1131 Some(b) => b,
1132 None => {
1133 return Ok(L2Snapshot {
1134 symbol,
1135 bids: vec![],
1136 asks: vec![],
1137 sequence: 0,
1138 })
1139 }
1140 };
1141 let bids = book
1142 .levels
1143 .first()
1144 .into_iter()
1145 .flatten()
1146 .filter_map(|level| {
1147 Some(L2Level {
1148 price: parse_decimal(&level.px)?,
1149 volume: parse_decimal(&level.sz)?,
1150 })
1151 })
1152 .collect();
1153 let asks = book
1154 .levels
1155 .get(1)
1156 .into_iter()
1157 .flatten()
1158 .filter_map(|level| {
1159 Some(L2Level {
1160 price: parse_decimal(&level.px)?,
1161 volume: parse_decimal(&level.sz)?,
1162 })
1163 })
1164 .collect();
1165 Ok(L2Snapshot {
1166 symbol: book.coin,
1167 bids,
1168 asks,
1169 sequence: book.time,
1170 })
1171 }
1172
1173 async fn get_price(&self, symbol: String) -> Result<Decimal, String> {
1175 let resp = self
1177 .info_post(serde_json::json!({"type": "allMids"}), 2, "get_price")
1178 .await?;
1179 parse_response::<HashMap<String, String>>(resp)
1180 .await?
1181 .get(&symbol)
1182 .and_then(|s| parse_decimal(s))
1183 .ok_or_else(|| format!("symbol {} not found", symbol))
1184 }
1185
1186 async fn get_predicted_fundings(&self) -> Result<Vec<PredictedFunding>, String> {
1189 let resp = self
1191 .info_post(
1192 serde_json::json!({"type": "predictedFundings"}),
1193 20,
1194 "get_predicted_fundings",
1195 )
1196 .await?;
1197 let data: PredictedFundingsResponse = parse_response(resp).await?;
1198 let mut result = Vec::new();
1199 for (symbol, venues) in data {
1200 for (venue, entry) in venues {
1201 let Some(entry) = entry else { continue };
1202 if let Some(funding_rate) = parse_decimal(&entry.funding_rate) {
1203 result.push(PredictedFunding {
1204 symbol: symbol.clone(),
1205 venue,
1206 funding_rate,
1207 next_funding_time_ms: entry.next_funding_time,
1208 });
1209 }
1210 }
1211 }
1212 Ok(result)
1213 }
1214}
1215
1216#[async_trait]
1217impl guilder_abstraction::ManageOrder for HyperliquidClient {
1218 async fn place_order(
1229 &self,
1230 symbol: String,
1231 side: OrderSide,
1232 price: Decimal,
1233 volume: Decimal,
1234 order_type: OrderType,
1235 time_in_force: TimeInForce,
1236 trigger_price: Option<Decimal>,
1237 reduce_only: bool,
1238 cloid: Option<String>,
1239 ) -> Result<OrderPlacement, String> {
1240 let asset_idx = self.get_asset_index(&symbol).await?;
1242 let is_buy = matches!(side, OrderSide::Buy);
1243
1244 let tif_str = match time_in_force {
1245 TimeInForce::Gtc => "Gtc",
1246 TimeInForce::Ioc => "Ioc",
1247 TimeInForce::Fok => "Fok",
1248 TimeInForce::Alo => "Alo",
1249 };
1250
1251 let cloid_hex = cloid.clone();
1252
1253 let is_trigger = matches!(order_type, OrderType::TakeProfit | OrderType::StopLoss);
1255
1256 let (order_msgpack, order_type_json) = if is_trigger {
1257 let trigger_px = trigger_price
1259 .ok_or_else(|| format!("{:?} order requires trigger_price to be set", order_type))?
1260 .normalize()
1261 .to_string();
1262
1263 let tpsl = match order_type {
1264 OrderType::TakeProfit => "tp",
1265 OrderType::StopLoss => "sl",
1266 _ => unreachable!(),
1267 };
1268
1269 let is_market = matches!(time_in_force, TimeInForce::Ioc);
1272
1273 let price_str = if is_market {
1276 trigger_px.clone()
1277 } else {
1278 price.normalize().to_string()
1279 };
1280
1281 let msgpack = build_trigger_order_msgpack(
1282 asset_idx,
1283 is_buy,
1284 &price_str,
1285 &volume.normalize().to_string(),
1286 reduce_only,
1287 &trigger_px,
1288 is_market,
1289 tpsl,
1290 cloid_hex.as_deref(),
1291 );
1292
1293 let json_type = if is_market {
1294 format!(
1295 r#"{{"trigger":{{"isMarket":true,"triggerPx":"{}","tpsl":"{}"}}}}"#,
1296 trigger_px, tpsl
1297 )
1298 } else {
1299 format!(
1300 r#"{{"trigger":{{"isMarket":false,"triggerPx":"{}","tpsl":"{}"}}}}"#,
1301 trigger_px, tpsl
1302 )
1303 };
1304
1305 (msgpack, json_type)
1306 } else {
1307 let (order_kind, tif_bytes) = match order_type {
1309 OrderType::Limit => ("limit", tif_str.as_bytes()),
1310 OrderType::Market => ("limit", b"Ioc".as_slice()),
1311 _ => unreachable!(),
1312 };
1313
1314 let price_str = price.normalize().to_string();
1315 let size_str = volume.normalize().to_string();
1316
1317 let msgpack = build_order_msgpack(
1318 asset_idx,
1319 is_buy,
1320 &price_str,
1321 &size_str,
1322 reduce_only,
1323 order_kind,
1324 tif_bytes,
1325 cloid_hex.as_deref(),
1326 );
1327
1328 let json_type = match order_type {
1329 OrderType::Limit => format!(r#"{{"limit":{{"tif":"{tif_str}"}}}}"#),
1330 OrderType::Market => r#"{"limit":{"tif":"Ioc"}}"#.to_string(),
1331 _ => unreachable!(),
1332 };
1333
1334 (msgpack, json_type)
1335 };
1336
1337 let mut action_msgpack = Vec::new();
1340 action_msgpack.push(0x83); action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("type".to_string())));
1342 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("order".to_string())));
1343 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("orders".to_string())));
1344 action_msgpack.push(0x91); action_msgpack.extend_from_slice(&order_msgpack);
1346 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("grouping".to_string())));
1347 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("na".to_string())));
1348
1349 let cloid_json = if let Some(ref c) = cloid_hex {
1350 format!(r#","c":"{c}""#)
1351 } else {
1352 String::new()
1353 };
1354
1355 let reduce_json = if reduce_only { "true" } else { "false" };
1356
1357 let price_for_json = if is_trigger && matches!(time_in_force, TimeInForce::Ioc) {
1358 if let Some(ref tp) = trigger_price {
1360 tp.normalize().to_string()
1361 } else {
1362 price.normalize().to_string()
1363 }
1364 } else {
1365 price.normalize().to_string()
1366 };
1367
1368 let action_json_str = format!(
1369 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"}}"#,
1370 price = price_for_json,
1371 size = volume.normalize().to_string(),
1372 );
1373
1374 let private_key = self.require_private_key()?;
1376 let nonce = std::time::SystemTime::now()
1377 .duration_since(std::time::UNIX_EPOCH)
1378 .unwrap()
1379 .as_millis() as u64;
1380
1381 let (r, s, v) = sign_with_msgpack(
1382 &action_msgpack,
1383 private_key,
1384 self.external_signer.as_ref(),
1385 nonce,
1386 None,
1387 self.network.eip712_source(),
1388 )
1389 .await?;
1390
1391 let payload_str = format!(
1392 r#"{{"action":{},"nonce":{},"signature":{{"r":"{}","s":"{}","v":{}}},"vaultAddress":null,"expiresAfter":null}}"#,
1393 action_json_str, nonce, r, s, v
1394 );
1395
1396 self.rest_limiter.acquire(1).await.map_err(|e| {
1397 format!(
1398 "rate_limited: rest_weight exhausted, retry_after_ms={}",
1399 e.retry_after.as_millis()
1400 )
1401 })?;
1402 self.address_limiter.acquire(1, false).await.map_err(|e| {
1403 format!(
1404 "rate_limited: address quota exhausted, retry_after_ms={}",
1405 e.retry_after.as_millis()
1406 )
1407 })?;
1408
1409 let resp = self
1410 .client
1411 .post(self.network.exchange_url())
1412 .header("Content-Type", "application/json")
1413 .body(payload_str)
1414 .send()
1415 .await
1416 .map_err(|e| e.to_string())?;
1417
1418 let status = resp.status();
1419 if !status.is_success() {
1420 let text = resp.text().await.map_err(|e| e.to_string())?;
1421 return Err(format!("HTTP {status}: {text}"));
1422 }
1423
1424 let body: Value = parse_response(resp).await?;
1425 if body["status"].as_str() == Some("err") {
1426 return Err(body["response"]
1427 .as_str()
1428 .unwrap_or("unknown error")
1429 .to_string());
1430 }
1431 let statuses = &body["response"]["data"]["statuses"][0];
1432
1433 let (oid, returned_cloid, timestamp_ms) = if let Some(resting) = statuses.get("resting") {
1434 let oid = resting["oid"]
1435 .as_i64()
1436 .ok_or_else(|| format!("resting status missing oid: {}", body))?;
1437 let returned_cloid = resting["cloid"].as_str().map(|s: &str| s.to_string());
1438 let ts = std::time::SystemTime::now()
1440 .duration_since(std::time::UNIX_EPOCH)
1441 .unwrap()
1442 .as_millis() as i64;
1443 (oid, returned_cloid, ts)
1444 } else if let Some(filled) = statuses.get("filled") {
1445 let oid = filled["oid"]
1446 .as_i64()
1447 .ok_or_else(|| format!("filled status missing oid: {}", body))?;
1448 let ts = std::time::SystemTime::now()
1450 .duration_since(std::time::UNIX_EPOCH)
1451 .unwrap()
1452 .as_millis() as i64;
1453 (oid, None, ts)
1454 } else if let Some(error) = statuses.get("error") {
1455 return Err(error
1456 .as_str()
1457 .unwrap_or("order rejected with unknown error")
1458 .to_string());
1459 } else {
1460 return Err(format!("unexpected order status: {}", body));
1461 };
1462
1463 Ok(OrderPlacement {
1464 order_id: oid,
1465 symbol,
1466 side,
1467 price,
1468 quantity: volume,
1469 timestamp_ms,
1470 cloid: returned_cloid.or(cloid),
1471 order_type,
1472 trigger_price,
1473 reduce_only,
1474 })
1475 }
1476
1477 async fn change_order_by_cloid(
1480 &self,
1481 cloid: i64,
1482 price: Decimal,
1483 volume: Decimal,
1484 ) -> Result<i64, String> {
1485 let user = self.require_user_address()?;
1486
1487 let resp = self
1489 .info_post(
1490 serde_json::json!({"type": "openOrders", "user": user}),
1491 20,
1492 "change_order_by_cloid",
1493 )
1494 .await?;
1495 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1496 let order = orders
1497 .iter()
1498 .find(|o| o.oid == cloid)
1499 .ok_or_else(|| format!("order {} not found", cloid))?;
1500
1501 let asset_idx = self.get_asset_index(&order.coin).await?;
1502 let is_buy = order.side == "B";
1503
1504 let action = serde_json::json!({
1505 "type": "batchModify",
1506 "modifies": [{
1507 "oid": cloid,
1508 "order": {
1509 "a": asset_idx,
1510 "b": is_buy,
1511 "p": price.to_string(),
1512 "s": volume.to_string(),
1513 "r": false,
1514 "t": {"limit": {"tif": "Gtc"}}
1515 }
1516 }]
1517 });
1518
1519 self.submit_signed_action(action, None).await?;
1520 Ok(cloid)
1521 }
1522
1523 async fn cancel_order_by_cloid(&self, cloid: String) -> Result<(), String> {
1528 let user = self.require_user_address()?;
1529
1530 let resp = self
1532 .info_post(
1533 serde_json::json!({"type": "openOrders", "user": user}),
1534 20,
1535 "cancel_order_by_cloid",
1536 )
1537 .await?;
1538 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1539 let order = orders
1540 .iter()
1541 .find(|o| o.cloid.as_ref() == Some(&cloid))
1542 .ok_or_else(|| format!("order with cloid {} not found", cloid))?;
1543
1544 let meta_resp = self
1546 .info_post(
1547 serde_json::json!({"type": "meta"}),
1548 20,
1549 "cancel_order_by_cloid",
1550 )
1551 .await?;
1552 let meta: MetaResponse = parse_response(meta_resp).await?;
1553
1554 let asset_idx = meta
1555 .universe
1556 .iter()
1557 .position(|a| a.name == order.coin)
1558 .ok_or_else(|| format!("asset {} not found in meta", order.coin))?;
1559
1560 let action = serde_json::json!({
1563 "type": "cancel",
1564 "cancels": [{"a": asset_idx, "o": order.oid}]
1565 });
1566
1567 self.submit_signed_action(action, None).await?;
1568 Ok(())
1569 }
1570
1571 async fn cancel_all_order(&self) -> Result<bool, String> {
1574 let user = self.require_user_address()?;
1575
1576 let resp = self
1578 .info_post(
1579 serde_json::json!({"type": "openOrders", "user": user}),
1580 20,
1581 "cancel_all_order",
1582 )
1583 .await?;
1584 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1585 if orders.is_empty() {
1586 return Ok(true);
1587 }
1588
1589 let meta_resp = self
1591 .info_post(serde_json::json!({"type": "meta"}), 20, "cancel_all_order")
1592 .await?;
1593 let meta: MetaResponse = parse_response(meta_resp).await?;
1594
1595 let cancels: Vec<Value> = orders
1596 .iter()
1597 .filter_map(|o| {
1598 let asset_idx = meta.universe.iter().position(|a| a.name == o.coin)?;
1599 Some(serde_json::json!({"a": asset_idx, "o": o.oid}))
1600 })
1601 .collect();
1602
1603 let action = serde_json::json!({"type": "cancel", "cancels": cancels});
1604 self.submit_signed_action(action, None).await?;
1605 Ok(true)
1606 }
1607}
1608
1609#[async_trait]
1610impl guilder_abstraction::SubscribeMarketData for HyperliquidClient {
1611 fn subscribe_l2_update(&self, symbol: String) -> BoxStream<Result<L2Update, String>> {
1612 Box::pin(stream::iter(vec![Err(format!(
1613 "subscribe_l2_update is unsupported for {symbol}; use subscribe_l2_snapshot"
1614 ))]))
1615 }
1616
1617 fn subscribe_l2_snapshot(&self, symbol: String) -> BoxStream<Result<L2Snapshot, String>> {
1618 let subscription = HyperliquidSubscription::L2Book { coin: symbol };
1619 Box::pin(managed_stream(
1620 self.market_ws_manager.clone(),
1621 subscription,
1622 |msg: HyperliquidWsInboundMessage| {
1623 if let Some(snapshot) = msg.as_l2_snapshot() {
1624 vec![Ok(snapshot)]
1625 } else {
1626 vec![]
1627 }
1628 },
1629 ))
1630 }
1631
1632 fn subscribe_asset_context(&self, symbol: String) -> BoxStream<Result<AssetContext, String>> {
1633 let subscription = HyperliquidSubscription::ActiveAssetCtx { coin: symbol };
1634 Box::pin(managed_stream(
1635 self.market_ws_manager.clone(),
1636 subscription,
1637 |msg: HyperliquidWsInboundMessage| {
1638 if let Some(ctx) = msg.as_asset_context() {
1639 vec![Ok(ctx)]
1640 } else {
1641 vec![]
1642 }
1643 },
1644 ))
1645 }
1646
1647 fn subscribe_liquidation(&self, user: String) -> BoxStream<Result<Liquidation, String>> {
1648 subscribe_user_stream(
1649 self,
1650 user.clone(),
1651 HyperliquidSubscription::UserEvents { user_addr: user },
1652 |msg: HyperliquidWsInboundMessage| {
1653 if let Some(liq) = msg.as_liquidation() {
1654 vec![Ok(liq)]
1655 } else {
1656 vec![]
1657 }
1658 },
1659 )
1660 }
1661
1662 fn subscribe_fill(&self, symbol: String) -> BoxStream<Result<Fill, String>> {
1663 let subscription = HyperliquidSubscription::Trades { coin: symbol };
1664 Box::pin(managed_stream(
1665 self.market_ws_manager.clone(),
1666 subscription,
1667 |msg: HyperliquidWsInboundMessage| {
1668 if let Some(fills) = msg.as_trades() {
1669 fills.into_iter().map(Ok).collect()
1670 } else {
1671 vec![]
1672 }
1673 },
1674 ))
1675 }
1676
1677 async fn unsubscribe_all(&self) {
1680 self.market_ws_manager.shutdown();
1681 }
1682}
1683
1684fn subscribe_user_stream<T, F>(
1685 client: &HyperliquidClient,
1686 user_addr: String,
1687 subscription: HyperliquidSubscription,
1688 parse: F,
1689) -> BoxStream<Result<T, String>>
1690where
1691 T: Send + 'static,
1692 F: Fn(HyperliquidWsInboundMessage) -> Vec<Result<T, String>> + Send + Sync + 'static,
1693{
1694 let manager = get_or_create_user_manager(
1695 &client.user_ws_managers,
1696 client.ws_send_limiter.clone(),
1697 user_addr,
1698 client.network.ws_url(),
1699 );
1700 Box::pin(async_stream::stream! {
1701 let stream = managed_stream(manager, subscription, parse);
1702 tokio::pin!(stream);
1703 while let Some(item) = stream.next().await {
1704 yield item;
1705 }
1706 })
1707}
1708
1709fn get_or_create_user_manager(
1710 user_ws_managers: &RwLock<HashMap<String, HyperliquidWsManager>>,
1711 ws_send_limiter: WsSendRateLimiter,
1712 user_addr: String,
1713 ws_url: &'static str,
1714) -> HyperliquidWsManager {
1715 {
1716 let managers = user_ws_managers.read().unwrap_or_else(|e| e.into_inner());
1717 if let Some(manager) = managers.get(&user_addr) {
1718 return manager.clone();
1719 }
1720 }
1721
1722 let mut managers = user_ws_managers.write().unwrap_or_else(|e| e.into_inner());
1723 managers
1724 .entry(user_addr.clone())
1725 .or_insert_with(|| HyperliquidWsManager::new(Some(user_addr), ws_send_limiter, ws_url))
1726 .clone()
1727}
1728
1729#[async_trait]
1730impl guilder_abstraction::GetAccountSnapshot for HyperliquidClient {
1731 async fn get_positions(&self) -> Result<Vec<Position>, String> {
1734 let user = self.require_user_address()?;
1735 let resp = self
1737 .info_post(
1738 serde_json::json!({"type": "clearinghouseState", "user": user}),
1739 2,
1740 "get_positions",
1741 )
1742 .await?;
1743 let state: ClearinghouseStateResponse = parse_response(resp).await?;
1744
1745 Ok(state
1746 .asset_positions
1747 .into_iter()
1748 .filter_map(|ap| {
1749 let p = ap.position;
1750 let size = parse_decimal(&p.szi)?;
1751 if size.is_zero() {
1752 return None;
1753 }
1754 let entry_price = p
1759 .entry_px
1760 .as_deref()
1761 .and_then(parse_decimal);
1762 let Some(entry_price) = entry_price else {
1763 return None;
1764 };
1765 let side = if size > Decimal::ZERO {
1766 OrderSide::Buy
1767 } else {
1768 OrderSide::Sell
1769 };
1770 Some(Position {
1771 symbol: p.coin,
1772 side,
1773 size: size.abs(),
1774 entry_price,
1775 })
1776 })
1777 .collect())
1778 }
1779
1780 async fn get_open_orders(&self) -> Result<Vec<OpenOrder>, String> {
1783 let user = self.require_user_address()?;
1784 let resp = self
1786 .info_post(
1787 serde_json::json!({"type": "openOrders", "user": user}),
1788 20,
1789 "get_open_orders",
1790 )
1791 .await?;
1792 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1793
1794 Ok(orders
1795 .into_iter()
1796 .filter_map(|o| {
1797 let price = parse_decimal(&o.limit_px)?;
1798 let quantity = parse_decimal(&o.orig_sz)?;
1799 let remaining = parse_decimal(&o.sz)?;
1800 let filled_quantity = quantity - remaining;
1801 let side = if o.side == "B" {
1802 OrderSide::Buy
1803 } else {
1804 OrderSide::Sell
1805 };
1806 Some(OpenOrder {
1807 order_id: o.oid,
1808 symbol: o.coin,
1809 side,
1810 price,
1811 quantity,
1812 filled_quantity,
1813 order_type: None, trigger_price: None, reduce_only: false, })
1817 })
1818 .collect())
1819 }
1820
1821 async fn get_balance(&self) -> Result<Vec<guilder_abstraction::AccountBalance>, String> {
1825 let user = self.require_user_address()?;
1826 let resp = self
1828 .info_post(
1829 serde_json::json!({"type": "spotClearinghouseState", "user": user}),
1830 15,
1831 "get_balance",
1832 )
1833 .await?;
1834
1835 #[allow(dead_code)]
1836 #[derive(Deserialize)]
1837 struct SpotBalance {
1838 coin: String,
1839 total: String,
1840 hold: String,
1841 #[serde(default)]
1842 token: Option<i32>,
1843 #[serde(default)]
1844 #[serde(rename = "entryNtl")]
1845 entry_ntl: Option<String>,
1846 }
1847
1848 let state: SpotStateResponse = parse_response(resp).await?;
1849
1850 let perp_parsed: Option<ClearinghouseStateResponse> = match self
1852 .info_post(
1853 serde_json::json!({"type": "clearinghouseState", "user": user}),
1854 2,
1855 "get_balance_margin",
1856 )
1857 .await
1858 {
1859 Ok(resp) => parse_response::<ClearinghouseStateResponse>(resp).await.ok(),
1860 Err(_) => None,
1861 };
1862
1863 let perp_margin_used: Option<Decimal> = perp_parsed.as_ref().and_then(|ch| {
1866 ch.margin_summary
1867 .total_margin_used
1868 .as_deref()
1869 .and_then(parse_decimal)
1870 });
1871
1872 let mut rows = map_spot_state(state, perp_margin_used)?;
1875
1876 let perp_state: ClearinghouseStateResponse = match perp_parsed {
1882 Some(ch) => ch,
1883 None => {
1884 let resp = self
1887 .info_post(
1888 serde_json::json!({"type": "clearinghouseState", "user": user}),
1889 2,
1890 "get_balance_perp_row",
1891 )
1892 .await?;
1893 parse_response::<ClearinghouseStateResponse>(resp).await?
1894 }
1895 };
1896 rows.push(map_perp_state(perp_state)?);
1897
1898 Ok(rows)
1899 }
1900
1901 async fn get_user_rate_limit(&self) -> Result<guilder_abstraction::UserRateLimit, String> {
1904 let user = self.require_user_address()?;
1905 let resp = self
1906 .info_post(
1907 serde_json::json!({"type": "userRateLimit", "user": user}),
1908 20,
1909 "get_user_rate_limit",
1910 )
1911 .await?;
1912 let val = parse_response::<Value>(resp).await?;
1913
1914 let cumulative_volume = val["cumVlm"]
1915 .as_str()
1916 .and_then(parse_decimal)
1917 .ok_or_else(|| "missing or invalid cumVlm".to_string())?;
1918 let requests_used = val["nRequestsUsed"]
1919 .as_i64()
1920 .ok_or_else(|| "missing or invalid nRequestsUsed".to_string())?;
1921 let requests_cap = val["nRequestsCap"]
1922 .as_i64()
1923 .ok_or_else(|| "missing or invalid nRequestsCap".to_string())?;
1924 let requests_surplus = val["nRequestsSurplus"]
1925 .as_i64()
1926 .ok_or_else(|| "missing or invalid nRequestsSurplus".to_string())?;
1927
1928 Ok(guilder_abstraction::UserRateLimit {
1929 cumulative_volume,
1930 requests_used,
1931 requests_cap,
1932 requests_surplus,
1933 })
1934 }
1935}
1936
1937#[async_trait]
1938impl guilder_abstraction::SubscribeUserEvents for HyperliquidClient {
1939 fn subscribe_user_fills(&self) -> BoxStream<Result<UserFill, String>> {
1940 let Some(addr) = self.user_address.as_ref() else {
1941 return Box::pin(stream::iter(vec![Err(
1942 "user address not registered".to_string()
1943 )]));
1944 };
1945 subscribe_user_stream(
1946 self,
1947 addr.clone(),
1948 HyperliquidSubscription::UserEvents {
1949 user_addr: addr.clone(),
1950 },
1951 |msg: HyperliquidWsInboundMessage| {
1952 if let Some(fills) = msg.as_user_fills() {
1953 fills.into_iter().map(Ok).collect()
1954 } else {
1955 vec![]
1956 }
1957 },
1958 )
1959 }
1960
1961 fn subscribe_order_updates(&self) -> BoxStream<Result<OrderUpdate, String>> {
1962 let Some(addr) = self.user_address.as_ref() else {
1963 return Box::pin(stream::iter(vec![Err(
1964 "user address not registered".to_string()
1965 )]));
1966 };
1967 subscribe_user_stream(
1968 self,
1969 addr.clone(),
1970 HyperliquidSubscription::OrderUpdates {
1971 user_addr: addr.clone(),
1972 },
1973 |msg: HyperliquidWsInboundMessage| {
1974 if let Some(updates) = msg.as_order_updates() {
1975 updates.into_iter().map(Ok).collect()
1976 } else {
1977 vec![]
1978 }
1979 },
1980 )
1981 }
1982
1983 fn subscribe_funding_payments(&self) -> BoxStream<Result<FundingPayment, String>> {
1984 let Some(addr) = self.user_address.as_ref() else {
1985 return Box::pin(stream::iter(vec![Err(
1986 "user address not registered".to_string()
1987 )]));
1988 };
1989 subscribe_user_stream(
1990 self,
1991 addr.clone(),
1992 HyperliquidSubscription::UserEvents {
1993 user_addr: addr.clone(),
1994 },
1995 |msg: HyperliquidWsInboundMessage| {
1996 if let Some(p) = msg.as_funding_payment() {
1997 vec![Ok(p)]
1998 } else {
1999 vec![]
2000 }
2001 },
2002 )
2003 }
2004
2005 fn subscribe_deposits(&self) -> BoxStream<Result<Deposit, String>> {
2006 let Some(addr) = self.user_address.as_ref() else {
2007 return Box::pin(stream::iter(vec![Err(
2008 "user address not registered".to_string()
2009 )]));
2010 };
2011 subscribe_user_stream(
2012 self,
2013 addr.clone(),
2014 HyperliquidSubscription::NonFundingLedger {
2015 user_addr: addr.clone(),
2016 },
2017 |msg: HyperliquidWsInboundMessage| {
2018 if let Some(deps) = msg.as_deposits() {
2019 deps.into_iter().map(Ok).collect()
2020 } else {
2021 vec![]
2022 }
2023 },
2024 )
2025 }
2026
2027 fn subscribe_withdrawals(&self) -> BoxStream<Result<Withdrawal, String>> {
2028 let Some(addr) = self.user_address.as_ref() else {
2029 return Box::pin(stream::iter(vec![Err(
2030 "user address not registered".to_string()
2031 )]));
2032 };
2033 subscribe_user_stream(
2034 self,
2035 addr.clone(),
2036 HyperliquidSubscription::NonFundingLedger {
2037 user_addr: addr.clone(),
2038 },
2039 |msg: HyperliquidWsInboundMessage| {
2040 if let Some(wds) = msg.as_withdrawals() {
2041 wds.into_iter().map(Ok).collect()
2042 } else {
2043 vec![]
2044 }
2045 },
2046 )
2047 }
2048
2049 fn subscribe_spot_balance(
2051 &self,
2052 ) -> BoxStream<Result<Vec<guilder_abstraction::AccountBalance>, String>> {
2053 let Some(addr) = self.user_address.as_ref() else {
2054 return Box::pin(stream::iter(vec![Err(
2055 "user address not registered".to_string()
2056 )]));
2057 };
2058 self.subscribe_spot_balance_with_address(addr.clone())
2059 }
2060
2061 fn subscribe_spot_balance_with_address(
2063 &self,
2064 address: String,
2065 ) -> BoxStream<Result<Vec<guilder_abstraction::AccountBalance>, String>> {
2066 subscribe_user_stream(
2067 self,
2068 address.clone(),
2069 HyperliquidSubscription::UserEvents { user_addr: address },
2070 |msg: HyperliquidWsInboundMessage| {
2071 if let Some(balances) = msg.as_spot_balance() {
2072 vec![Ok(balances)]
2073 } else {
2074 vec![]
2075 }
2076 },
2077 )
2078 }
2079
2080 async fn unsubscribe_user_events(&self) {
2081 if let Some(addr) = &self.user_address {
2083 self.market_ws_manager.unsubscribe_user(addr);
2084 }
2085 let managers = self
2087 .user_ws_managers
2088 .read()
2089 .unwrap_or_else(|e| e.into_inner());
2090 for (addr, manager) in managers.iter() {
2091 manager.unsubscribe_user(addr);
2092 }
2093 }
2094}
2095
2096#[async_trait]
2097impl guilder_abstraction::SubscribeMarketDataOps for HyperliquidClient {
2098 async fn unsubscribe_market_data(&self, symbol: String) {
2099 self.market_ws_manager.unsubscribe_by_coin(&symbol);
2100 }
2101}
2102
2103#[cfg(test)]
2104mod msgpack_tests {
2105 use super::*;
2106 use serde_json::json;
2107
2108 #[test]
2109 fn test_eip712_digest_known_answers() {
2110 let action = json!({
2118 "type": "order",
2119 "orders": [{
2120 "a": 1, "b": true, "p": "1500.5", "s": "0.05",
2121 "r": false, "t": {"limit": {"tif": "Gtc"}}
2122 }],
2123 "grouping": "na"
2124 });
2125 let nonce: u64 = 1758572400123;
2126 let msgpack = action_to_canonical_msgpack(&action).unwrap();
2127
2128 let mainnet = compute_eip712_digest(&msgpack, nonce, None, "a").unwrap();
2129 assert_eq!(
2130 hex::encode(mainnet),
2131 "28cd97dd515629633af463bd7edaab14e61b3941b638d410c317cac7e0aed860"
2132 );
2133
2134 let testnet = compute_eip712_digest(&msgpack, nonce, None, "b").unwrap();
2135 assert_eq!(
2136 hex::encode(testnet),
2137 "d452e53f806773ca6d7ab5d10147518bff0d8b64a4404dcb062e3d2fb3d85c0b"
2138 );
2139
2140 assert_ne!(mainnet, testnet);
2141 assert_eq!(HyperliquidNetwork::Mainnet.eip712_source(), "a");
2142 assert_eq!(HyperliquidNetwork::Testnet.eip712_source(), "b");
2143 }
2144
2145 #[test]
2146 fn test_msgpack_null() {
2147 let result = value_to_msgpack(&Value::Null);
2148 assert_eq!(result, vec![0xc0]);
2149 }
2150
2151 #[test]
2152 fn test_msgpack_bool() {
2153 assert_eq!(value_to_msgpack(&Value::Bool(true)), vec![0xc3]);
2154 assert_eq!(value_to_msgpack(&Value::Bool(false)), vec![0xc2]);
2155 }
2156
2157 #[test]
2158 fn test_msgpack_positive_fixint() {
2159 assert_eq!(value_to_msgpack(&json!(0)), vec![0x00]);
2161 assert_eq!(value_to_msgpack(&json!(1)), vec![0x01]);
2162 assert_eq!(value_to_msgpack(&json!(127)), vec![0x7f]);
2163 }
2164
2165 #[test]
2166 fn test_msgpack_uint8() {
2167 assert_eq!(value_to_msgpack(&json!(128)), vec![0xcc, 0x80]);
2169 assert_eq!(value_to_msgpack(&json!(255)), vec![0xcc, 0xff]);
2170 }
2171
2172 #[test]
2173 fn test_msgpack_uint16() {
2174 assert_eq!(value_to_msgpack(&json!(256)), vec![0xcd, 0x01, 0x00]);
2176 assert_eq!(value_to_msgpack(&json!(65535)), vec![0xcd, 0xff, 0xff]);
2177 }
2178
2179 #[test]
2180 fn test_msgpack_uint32() {
2181 assert_eq!(
2183 value_to_msgpack(&json!(65536)),
2184 vec![0xce, 0x00, 0x01, 0x00, 0x00]
2185 );
2186 assert_eq!(
2187 value_to_msgpack(&json!(4294967295u64)),
2188 vec![0xce, 0xff, 0xff, 0xff, 0xff]
2189 );
2190 }
2191
2192 #[test]
2193 fn test_msgpack_uint64() {
2194 let big: u64 = 4294967296;
2196 assert_eq!(
2197 value_to_msgpack(&json!(big)),
2198 vec![0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]
2199 );
2200 }
2201
2202 #[test]
2203 fn test_msgpack_negative_fixint() {
2204 assert_eq!(value_to_msgpack(&json!(-1)), vec![0xff]);
2206 assert_eq!(value_to_msgpack(&json!(-32)), vec![0xe0]);
2207 }
2208
2209 #[test]
2210 fn test_msgpack_int8() {
2211 assert_eq!(value_to_msgpack(&json!(-33)), vec![0xd0, 0xdf]);
2213 assert_eq!(value_to_msgpack(&json!(-128)), vec![0xd0, 0x80]);
2214 }
2215
2216 #[test]
2217 fn test_msgpack_int16() {
2218 assert_eq!(value_to_msgpack(&json!(-129)), vec![0xd1, 0xff, 0x7f]);
2220 assert_eq!(value_to_msgpack(&json!(-32768)), vec![0xd1, 0x80, 0x00]);
2221 }
2222
2223 #[test]
2224 fn test_msgpack_int32() {
2225 assert_eq!(
2227 value_to_msgpack(&json!(-32769)),
2228 vec![0xd2, 0xff, 0xff, 0x7f, 0xff]
2229 );
2230 assert_eq!(
2231 value_to_msgpack(&json!(-2147483648i64)),
2232 vec![0xd2, 0x80, 0x00, 0x00, 0x00]
2233 );
2234 }
2235
2236 #[test]
2237 fn test_msgpack_int64() {
2238 let val: i64 = -2147483649;
2239 let result = value_to_msgpack(&json!(val));
2240 assert_eq!(result[0], 0xd3); assert_eq!(result.len(), 9);
2242 }
2243
2244 #[test]
2245 fn test_msgpack_float() {
2246 let result = value_to_msgpack(&json!(3.14));
2247 assert_eq!(result[0], 0xcb); assert_eq!(result.len(), 9);
2249 }
2250
2251 #[test]
2252 fn test_msgpack_fixstr() {
2253 assert_eq!(value_to_msgpack(&json!("")), vec![0xa0]);
2255 assert_eq!(value_to_msgpack(&json!("hello")), {
2256 let mut expected = vec![0xa5];
2257 expected.extend_from_slice(b"hello");
2258 expected
2259 });
2260 let s = "a".repeat(31);
2261 let result = value_to_msgpack(&json!(s));
2262 assert_eq!(result[0], 0xbf); assert_eq!(result.len(), 32);
2264 }
2265
2266 #[test]
2267 fn test_msgpack_str8() {
2268 let s = "a".repeat(32);
2269 let result = value_to_msgpack(&json!(s));
2270 assert_eq!(result[0], 0xd9); assert_eq!(result[1], 32);
2272 assert_eq!(result.len(), 34);
2273 }
2274
2275 #[test]
2276 fn test_msgpack_fixarray() {
2277 assert_eq!(value_to_msgpack(&json!([])), vec![0x90]);
2279 let result = value_to_msgpack(&json!([1, 2, 3]));
2280 assert_eq!(result[0], 0x93);
2281 assert_eq!(result, vec![0x93, 0x01, 0x02, 0x03]);
2282 }
2283
2284 #[test]
2285 fn test_msgpack_fixmap() {
2286 assert_eq!(value_to_msgpack(&json!({})), vec![0x80]);
2288 let result = value_to_msgpack(&json!({"a": 1}));
2289 assert_eq!(result[0], 0x81); assert_eq!(result, {
2291 let mut expected = vec![0x81];
2292 expected.extend_from_slice(&value_to_msgpack(&json!("a")));
2293 expected.extend_from_slice(&value_to_msgpack(&json!(1)));
2294 expected
2295 });
2296 }
2297
2298 #[test]
2299 fn test_msgmap_preserves_insertion_order() {
2300 let val = json!({
2302 "z": 1,
2303 "a": 2,
2304 "m": 3
2305 });
2306 let result = value_to_msgpack(&val);
2307 assert_eq!(result[0], 0x83);
2309 assert_eq!(result[1], 0xa1); assert_eq!(result[2], b'z');
2312 }
2313
2314 #[test]
2315 fn test_msgpack_mixed_array() {
2316 let val = json!([null, true, false, 42, "hi", [1, 2]]);
2317 let result = value_to_msgpack(&val);
2318 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);
2325 assert_eq!(result[6], b'h');
2326 assert_eq!(result[7], b'i');
2327 }
2328
2329 #[test]
2330 fn test_build_order_msgpack_without_cloid() {
2331 let result = build_order_msgpack(
2332 0, true, "1000", "0.1", false, "limit", b"gtc", None, );
2341 assert_eq!(result[0], 0x86);
2343 }
2344
2345 #[test]
2346 fn test_build_order_msgpack_with_cloid() {
2347 let result = build_order_msgpack(
2348 0, true, "1000", "0.1", false, "limit", b"gtc", Some("my-cloid"), );
2357 assert_eq!(result[0], 0x87);
2359 }
2360
2361 #[test]
2362 fn test_action_to_canonical_msgpack() {
2363 let action = json!({
2364 "type": "order",
2365 "orders": [{"a": 0, "b": true, "p": "1000", "s": "0.1", "r": false, "t": {"limit": {"tif": "gtc"}}}],
2366 "grouping": "na"
2367 });
2368 let result = action_to_canonical_msgpack(&action).unwrap();
2369 assert_eq!(result[0], 0x83);
2371 }
2372
2373 #[test]
2374 fn test_msgpack_matches_rmp_serde_for_simple_values() {
2375 use rmp_serde::to_vec;
2377
2378 for val in [
2379 json!(0),
2380 json!(127),
2381 json!(255),
2382 json!(1000),
2383 json!(-1),
2384 json!(-32),
2385 json!(-128),
2386 ] {
2387 let ours = value_to_msgpack(&val);
2388 let theirs = to_vec(&val).unwrap();
2389 assert_eq!(
2390 ours, theirs,
2391 "mismatch for {}: ours={:?}, rmp={:?}",
2392 val, ours, theirs
2393 );
2394 }
2395 }
2396
2397 #[test]
2398 fn test_msgpack_string_encoding() {
2399 use rmp_serde::to_vec;
2400 for val in [
2401 json!(""),
2402 json!("a"),
2403 json!("hello world"),
2404 json!("BTC-USD"),
2405 ] {
2406 let ours = value_to_msgpack(&val);
2407 let theirs = to_vec(&val).unwrap();
2408 assert_eq!(
2409 ours, theirs,
2410 "mismatch for {}: ours={:?}, rmp={:?}",
2411 val, ours, theirs
2412 );
2413 }
2414 }
2415
2416 #[test]
2417 fn test_msgpack_bool_encoding() {
2418 use rmp_serde::to_vec;
2419 let theirs = to_vec(&json!(true)).unwrap();
2420 assert_eq!(value_to_msgpack(&json!(true)), theirs);
2421 let theirs = to_vec(&json!(false)).unwrap();
2422 assert_eq!(value_to_msgpack(&json!(false)), theirs);
2423 }
2424
2425 #[test]
2426 fn test_msgpack_null_encoding() {
2427 use rmp_serde::to_vec;
2428 let theirs = to_vec(&Value::Null).unwrap();
2429 assert_eq!(value_to_msgpack(&Value::Null), theirs);
2430 }
2431
2432 #[test]
2433 fn test_msgpack_nested_object() {
2434 let val = json!({
2435 "outer": {
2436 "inner": 42
2437 }
2438 });
2439 let result = value_to_msgpack(&val);
2440 assert_eq!(result[0], 0x81); }
2442
2443 #[test]
2444 fn test_msgpack_empty_containers() {
2445 assert_eq!(value_to_msgpack(&json!([])), vec![0x90]);
2446 assert_eq!(value_to_msgpack(&json!({})), vec![0x80]);
2447 }
2448}
2449
2450#[async_trait::async_trait]
2451impl guilder_abstraction::ListingEventSource for HyperliquidClient {
2452 async fn get_listing_events(&self) -> Result<Vec<guilder_abstraction::ListingEvent>, String> {
2460 let resp = self
2462 .info_post(
2463 serde_json::json!({"type": "meta"}),
2464 20,
2465 "get_listing_events",
2466 )
2467 .await?;
2468 let meta = parse_response::<MetaResponse>(resp).await?;
2469 Ok(meta
2470 .universe
2471 .into_iter()
2472 .map(|a| guilder_abstraction::ListingEvent {
2473 ticker: a.name,
2474 exchange: "hyperliquid_perp".to_string(),
2475 event: if a.is_delisted { "delist" } else { "list" }.to_string(),
2476 event_time: String::new(),
2477 })
2478 .collect())
2479 }
2480
2481 async fn get_current_universe(&self) -> Result<Vec<guilder_abstraction::SymbolStatus>, String> {
2482 let resp = self
2484 .info_post(
2485 serde_json::json!({"type": "meta"}),
2486 20,
2487 "get_current_universe",
2488 )
2489 .await?;
2490 let meta = parse_response::<MetaResponse>(resp).await?;
2491 Ok(meta
2492 .universe
2493 .into_iter()
2494 .map(|a| guilder_abstraction::SymbolStatus {
2495 ticker: a.name,
2496 exchange: "hyperliquid_perp".to_string(),
2497 is_delisted: a.is_delisted,
2498 })
2499 .collect())
2500 }
2501}
2502
2503#[cfg(test)]
2504mod spot_state_tests {
2505 use super::*;
2506
2507 const TESTNET_SHAPE: &str =
2512 r#"{"balances":[{"coin":"USDC","token":0,"total":"969.0","hold":"0.0","entryNtl":"0.0"}]}"#;
2513
2514 const MAINNET_SHAPE: &str = r#"{"balances":[{"coin":"USDC","token":0,"total":"969.0","hold":"10.0","entryNtl":"0.0"}],"tokenToAvailableAfterMaintenance":[[0,"955.0"]]}"#;
2516
2517 #[test]
2518 fn testnet_spot_state_without_maintenance_map_parses() {
2519 let state: SpotStateResponse =
2520 serde_json::from_str(TESTNET_SHAPE).expect("testnet shape must deserialize");
2521 let balances = map_spot_state(state, None).expect("mapping must succeed");
2522 assert_eq!(balances.len(), 1);
2523 let usdc = &balances[0];
2524 assert_eq!(usdc.token, "USDC");
2525 assert_eq!(usdc.equity.to_string(), "969.0");
2526 assert_eq!(usdc.hold.to_string(), "0.0");
2527 assert_eq!(usdc.free.to_string(), "969.0");
2528 assert_eq!(usdc.usable.to_string(), "969.0");
2530 assert_eq!(usdc.safe, None);
2531 assert_eq!(usdc.maintenance, None);
2532 }
2533
2534 #[test]
2535 fn mainnet_spot_state_with_maintenance_map_parses() {
2536 let state: SpotStateResponse =
2537 serde_json::from_str(MAINNET_SHAPE).expect("mainnet shape must deserialize");
2538 let balances = map_spot_state(state, None).expect("mapping must succeed");
2539 let usdc = &balances[0];
2540 assert_eq!(usdc.equity.to_string(), "969.0");
2541 assert_eq!(usdc.free.to_string(), "959.0");
2542 assert_eq!(usdc.usable.to_string(), "955.0");
2544 assert_eq!(usdc.safe.map(|d| d.to_string()), Some("955.0".to_string()));
2545 assert_eq!(
2546 usdc.maintenance.map(|d| d.to_string()),
2547 Some("14.0".to_string())
2548 );
2549 }
2550
2551 #[test]
2552 fn usdc_balance_carries_perp_margin_used() {
2553 let state: SpotStateResponse =
2554 serde_json::from_str(TESTNET_SHAPE).expect("testnet shape must deserialize");
2555 let balances = map_spot_state(state, Some(Decimal::from(2))).expect("mapping must succeed");
2556 let usdc = &balances[0];
2557 assert_eq!(
2558 usdc.margin_used.map(|d| d.to_string()),
2559 Some("2".to_string())
2560 );
2561 }
2562
2563 #[test]
2567 fn balance_rows_end_with_marked_perp_row() {
2568 let state: SpotStateResponse =
2569 serde_json::from_str(TESTNET_SHAPE).expect("testnet shape must deserialize");
2570 let perp: ClearinghouseStateResponse = serde_json::from_str(
2571 r#"{"marginSummary":{"accountValue":"30.0","totalMarginUsed":"5.0"},"assetPositions":[{"position":{"coin":"BTC","szi":"0.1","entryPx":"100","unrealizedPnl":"2.0"}}]}"#,
2572 )
2573 .expect("perp shape must deserialize");
2574 let mut rows =
2575 map_spot_state(state, Some(Decimal::from(2))).expect("spot rows must map");
2576 rows.push(map_perp_state(perp).expect("perp row must map"));
2577
2578 assert_eq!(rows.len(), 2);
2579 assert_eq!(rows[0].token, "USDC");
2580 assert_eq!(rows[0].margin_used.map(|d| d.to_string()), Some("2".to_string()));
2581 assert_eq!(rows[1].token, crate::PERP_LEDGER_TOKEN);
2582 assert_eq!(rows[1].equity.to_string(), "30.0");
2583 assert_eq!(rows[1].free.to_string(), "25.0");
2584 assert_eq!(rows[1].usable.to_string(), "25.0");
2585 assert_eq!(
2586 rows[1].margin_used.map(|d| d.to_string()),
2587 Some("5.0".to_string())
2588 );
2589 assert_eq!(rows[1].settled_usd.map(|d| d.to_string()), Some("28.0".into()));
2592 assert!(rows[..rows.len() - 1]
2594 .iter()
2595 .all(|b| b.token != crate::PERP_LEDGER_TOKEN));
2596 }
2597
2598 #[test]
2602 fn perp_margin_account_maps_to_usdc_row() {
2603 let state: ClearinghouseStateResponse = serde_json::from_str(
2604 r#"{"marginSummary":{"accountValue":"30.0","totalNtlPos":"0.0","totalRawUsd":"-70.0","totalMarginUsed":"0.0"},"assetPositions":[]}"#,
2605 )
2606 .expect("perp shape must deserialize");
2607 let usdc = map_perp_state(state).expect("perp mapping must succeed");
2608 assert_eq!(usdc.token, crate::PERP_LEDGER_TOKEN);
2611 assert_eq!(usdc.settled_usd.map(|d| d.to_string()), Some("30.0".into()));
2614 assert_eq!(usdc.equity.to_string(), "30.0"); assert_eq!(usdc.free.to_string(), "30.0"); assert_eq!(usdc.usable.to_string(), "30.0");
2617 assert_eq!(
2618 usdc.margin_used.map(|d| d.to_string()),
2619 Some("0.0".to_string())
2620 );
2621 }
2622}