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}
411
412#[derive(Deserialize)]
413#[serde(rename_all = "camelCase")]
414struct RestOpenOrder {
415 coin: String,
416 side: String,
417 limit_px: String,
418 sz: String,
419 oid: i64,
420 orig_sz: String,
421 cloid: Option<String>,
422}
423
424type PredictedFundingsResponse = Vec<(String, Vec<(String, Option<PredictedFundingEntry>)>)>;
427
428#[derive(Deserialize)]
429#[serde(rename_all = "camelCase")]
430struct PredictedFundingEntry {
431 funding_rate: String,
432 next_funding_time: i64,
433}
434
435#[derive(Deserialize)]
442pub(crate) struct SpotStateResponse {
443 pub balances: Vec<SpotBalance>,
444 #[serde(default, rename = "tokenToAvailableAfterMaintenance")]
445 pub token_to_available_after_maintenance: Vec<(i32, String)>,
446}
447
448#[derive(Deserialize)]
449pub(crate) struct SpotBalance {
450 pub coin: String,
451 pub total: String,
452 pub hold: String,
453 #[serde(default)]
454 pub token: Option<i32>,
455 #[serde(default)]
456 #[serde(rename = "entryNtl")]
457 pub entry_ntl: Option<String>,
458}
459
460pub(crate) fn map_spot_state(
464 state: SpotStateResponse,
465 perp_margin_used: Option<Decimal>,
466) -> Result<Vec<guilder_abstraction::AccountBalance>, String> {
467 let safe_map: HashMap<i32, Decimal> = state
470 .token_to_available_after_maintenance
471 .into_iter()
472 .filter_map(|(token_id, value)| parse_decimal(&value).map(|v| (token_id, v)))
473 .collect();
474
475 state
476 .balances
477 .into_iter()
478 .map(|balance| {
479 let equity =
480 parse_decimal(&balance.total).ok_or_else(|| "invalid total balance".to_string())?;
481 let hold =
482 parse_decimal(&balance.hold).ok_or_else(|| "invalid hold balance".to_string())?;
483 let free = equity - hold;
484
485 let safe = balance
486 .token
487 .and_then(|token_id| safe_map.get(&token_id).copied());
488 let usable = match safe {
489 Some(s) => std::cmp::min(free, s),
490 None => free,
491 };
492 let maintenance = safe.map(|s| equity - s);
493
494 let margin_used = if balance.coin == "USDC" {
496 perp_margin_used
497 } else {
498 None
499 };
500
501 Ok(guilder_abstraction::AccountBalance {
502 token: balance.coin,
503 equity,
504 free,
505 safe,
506 usable,
507 hold,
508 margin_used,
509 maintenance,
510 settled_usd: None,
511 })
512 })
513 .collect()
514}
515
516pub(crate) fn map_perp_state(
521 state: ClearinghouseStateResponse,
522) -> Result<guilder_abstraction::AccountBalance, String> {
523 let equity = parse_decimal(&state.margin_summary.account_value)
524 .ok_or_else(|| "invalid accountValue".to_string())?;
525 let margin_used = state
526 .margin_summary
527 .total_margin_used
528 .as_deref()
529 .and_then(parse_decimal)
530 .unwrap_or(Decimal::ZERO);
531 let free = equity - margin_used;
532 let settled_usd = state
533 .margin_summary
534 .total_raw_usd
535 .as_deref()
536 .and_then(parse_decimal);
537 Ok(guilder_abstraction::AccountBalance {
538 token: crate::PERP_LEDGER_TOKEN.to_string(),
539 equity,
540 free,
541 safe: None,
542 usable: free,
543 hold: Decimal::ZERO,
544 margin_used: Some(margin_used),
545 maintenance: Some(equity - margin_used),
546 settled_usd,
547 })
548}
549
550fn parse_decimal(s: &str) -> Option<Decimal> {
551 Decimal::from_str(s).ok()
552}
553
554fn keccak256(data: &[u8]) -> [u8; 32] {
555 use sha3::{Digest, Keccak256};
556 Keccak256::digest(data).into()
557}
558
559fn hyperliquid_domain_separator() -> [u8; 32] {
561 let type_hash = keccak256(
562 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
563 );
564 let name_hash = keccak256(b"Exchange");
565 let version_hash = keccak256(b"1");
566 let mut chain_id = [0u8; 32];
567 chain_id[28..32].copy_from_slice(&1337u32.to_be_bytes());
568 let verifying_contract = [0u8; 32];
569
570 let mut data = [0u8; 160];
571 data[..32].copy_from_slice(&type_hash);
572 data[32..64].copy_from_slice(&name_hash);
573 data[64..96].copy_from_slice(&version_hash);
574 data[96..128].copy_from_slice(&chain_id);
575 data[128..160].copy_from_slice(&verifying_contract);
576 keccak256(&data)
577}
578
579fn value_to_msgpack(val: &Value) -> Vec<u8> {
582 match val {
583 Value::Null => vec![0xc0],
584 Value::Bool(true) => vec![0xc3],
585 Value::Bool(false) => vec![0xc2],
586 Value::Number(n) => {
587 if let Some(i) = n.as_i64() {
588 if i >= 0 {
589 if i <= 127 {
590 vec![i as u8]
591 } else if i <= 255 {
592 vec![0xcc, i as u8]
593 } else if i <= 65535 {
594 let mut buf = vec![0xcd];
595 buf.extend_from_slice(&(i as u16).to_be_bytes());
596 buf
597 } else if i <= 4294967295 {
598 let mut buf = vec![0xce];
599 buf.extend_from_slice(&(i as u32).to_be_bytes());
600 buf
601 } else {
602 let mut buf = vec![0xcf];
603 buf.extend_from_slice(&(i as u64).to_be_bytes());
604 buf
605 }
606 } else if i >= -32 {
607 vec![0xe0 | (i as u8)]
608 } else if i >= -128 {
609 vec![0xd0, i as i8 as u8]
610 } else if i >= -32768 {
611 let mut buf = vec![0xd1];
612 buf.extend_from_slice(&(i as i16).to_be_bytes());
613 buf
614 } else if i >= -2147483648 {
615 let mut buf = vec![0xd2];
616 buf.extend_from_slice(&(i as i32).to_be_bytes());
617 buf
618 } else {
619 let mut buf = vec![0xd3];
620 buf.extend_from_slice(&i.to_be_bytes());
621 buf
622 }
623 } else if let Some(f) = n.as_f64() {
624 let mut buf = vec![0xcb];
625 buf.extend_from_slice(&f.to_be_bytes());
626 buf
627 } else {
628 let u = n.as_u64().unwrap();
629 if u <= 127 {
630 vec![u as u8]
631 } else if u <= 255 {
632 vec![0xcc, u as u8]
633 } else if u <= 65535 {
634 let mut buf = vec![0xcd];
635 buf.extend_from_slice(&(u as u16).to_be_bytes());
636 buf
637 } else if u <= 4294967295 {
638 let mut buf = vec![0xce];
639 buf.extend_from_slice(&(u as u32).to_be_bytes());
640 buf
641 } else {
642 let mut buf = vec![0xcf];
643 buf.extend_from_slice(&u.to_be_bytes());
644 buf
645 }
646 }
647 }
648 Value::String(s) => {
649 let bytes = s.as_bytes();
650 let len = bytes.len();
651 let mut buf = Vec::new();
652 if len <= 31 {
653 buf.push(0xa0 | (len as u8));
654 } else if len <= 255 {
655 buf.push(0xd9);
656 buf.push(len as u8);
657 } else if len <= 65535 {
658 buf.push(0xda);
659 buf.extend_from_slice(&(len as u16).to_be_bytes());
660 } else {
661 buf.push(0xdb);
662 buf.extend_from_slice(&(len as u32).to_be_bytes());
663 }
664 buf.extend_from_slice(bytes);
665 buf
666 }
667 Value::Array(arr) => {
668 let len = arr.len();
669 let mut buf = Vec::new();
670 if len <= 15 {
671 buf.push(0x90 | (len as u8));
672 } else if len <= 65535 {
673 buf.push(0xdc);
674 buf.extend_from_slice(&(len as u16).to_be_bytes());
675 } else {
676 buf.push(0xdd);
677 buf.extend_from_slice(&(len as u32).to_be_bytes());
678 }
679 for item in arr {
680 buf.extend_from_slice(&value_to_msgpack(item));
681 }
682 buf
683 }
684 Value::Object(map) => {
685 let len = map.len();
686 let mut buf = Vec::new();
687 if len <= 15 {
688 buf.push(0x80 | (len as u8));
689 } else if len <= 65535 {
690 buf.push(0xde);
691 buf.extend_from_slice(&(len as u16).to_be_bytes());
692 } else {
693 buf.push(0xdf);
694 buf.extend_from_slice(&(len as u32).to_be_bytes());
695 }
696 for (key, value) in map {
697 buf.extend_from_slice(&value_to_msgpack(&Value::String(key.clone())));
698 buf.extend_from_slice(&value_to_msgpack(value));
699 }
700 buf
701 }
702 }
703}
704
705fn action_to_canonical_msgpack(action: &Value) -> Result<Vec<u8>, String> {
708 Ok(value_to_msgpack(action))
709}
710
711fn build_order_msgpack(
714 asset_idx: usize,
715 is_buy: bool,
716 price: &str,
717 size: &str,
718 reduce_only: bool,
719 order_kind: &str,
720 tif: &[u8],
721 cloid: Option<&str>,
722) -> Vec<u8> {
723 let field_count = if cloid.is_some() { 7 } else { 6 };
724 let mut buf = Vec::new();
725 buf.push(0x80 | (field_count as u8)); buf.extend_from_slice(&value_to_msgpack(&Value::String("a".to_string())));
729 buf.extend_from_slice(&value_to_msgpack(&Value::Number(serde_json::Number::from(
730 asset_idx,
731 ))));
732
733 buf.extend_from_slice(&value_to_msgpack(&Value::String("b".to_string())));
735 buf.push(if is_buy { 0xc3 } else { 0xc2 });
736
737 buf.extend_from_slice(&value_to_msgpack(&Value::String("p".to_string())));
739 buf.extend_from_slice(&value_to_msgpack(&Value::String(price.to_string())));
740
741 buf.extend_from_slice(&value_to_msgpack(&Value::String("s".to_string())));
743 buf.extend_from_slice(&value_to_msgpack(&Value::String(size.to_string())));
744
745 buf.extend_from_slice(&value_to_msgpack(&Value::String("r".to_string())));
747 buf.push(if reduce_only { 0xc3 } else { 0xc2 });
748
749 buf.extend_from_slice(&value_to_msgpack(&Value::String("t".to_string())));
751 buf.push(0x81);
753 buf.extend_from_slice(&value_to_msgpack(&Value::String(order_kind.to_string())));
754 buf.push(0x81);
756 buf.extend_from_slice(&value_to_msgpack(&Value::String("tif".to_string())));
757 buf.extend_from_slice(&value_to_msgpack(&Value::String(
758 String::from_utf8_lossy(tif).to_string(),
759 )));
760
761 if let Some(c) = cloid {
763 buf.extend_from_slice(&value_to_msgpack(&Value::String("c".to_string())));
764 buf.extend_from_slice(&value_to_msgpack(&Value::String(c.to_string())));
765 }
766
767 buf
768}
769
770fn build_trigger_order_msgpack(
773 asset_idx: usize,
774 is_buy: bool,
775 price: &str,
776 size: &str,
777 reduce_only: bool,
778 trigger_px: &str,
779 is_market: bool,
780 tpsl: &str,
781 cloid: Option<&str>,
782) -> Vec<u8> {
783 let field_count = if cloid.is_some() { 7 } else { 6 };
784 let mut buf = Vec::new();
785 buf.push(0x80 | (field_count as u8)); buf.extend_from_slice(&value_to_msgpack(&Value::String("a".to_string())));
789 buf.extend_from_slice(&value_to_msgpack(&Value::Number(serde_json::Number::from(
790 asset_idx,
791 ))));
792
793 buf.extend_from_slice(&value_to_msgpack(&Value::String("b".to_string())));
795 buf.push(if is_buy { 0xc3 } else { 0xc2 });
796
797 buf.extend_from_slice(&value_to_msgpack(&Value::String("p".to_string())));
799 buf.extend_from_slice(&value_to_msgpack(&Value::String(price.to_string())));
800
801 buf.extend_from_slice(&value_to_msgpack(&Value::String("s".to_string())));
803 buf.extend_from_slice(&value_to_msgpack(&Value::String(size.to_string())));
804
805 buf.extend_from_slice(&value_to_msgpack(&Value::String("r".to_string())));
807 buf.push(if reduce_only { 0xc3 } else { 0xc2 });
808
809 buf.extend_from_slice(&value_to_msgpack(&Value::String("t".to_string())));
811 buf.push(0x81); buf.extend_from_slice(&value_to_msgpack(&Value::String("trigger".to_string())));
813 buf.push(0x83);
815 buf.extend_from_slice(&value_to_msgpack(&Value::String("isMarket".to_string())));
816 buf.push(if is_market { 0xc3 } else { 0xc2 });
817 buf.extend_from_slice(&value_to_msgpack(&Value::String("triggerPx".to_string())));
818 buf.extend_from_slice(&value_to_msgpack(&Value::String(trigger_px.to_string())));
819 buf.extend_from_slice(&value_to_msgpack(&Value::String("tpsl".to_string())));
820 buf.extend_from_slice(&value_to_msgpack(&Value::String(tpsl.to_string())));
821
822 if let Some(c) = cloid {
824 buf.extend_from_slice(&value_to_msgpack(&Value::String("c".to_string())));
825 buf.extend_from_slice(&value_to_msgpack(&Value::String(c.to_string())));
826 }
827
828 buf
829}
830
831fn compute_eip712_digest(
836 msgpack: &[u8],
837 nonce: u64,
838 vault_address: Option<&str>,
839 source: &str,
840) -> Result<[u8; 32], String> {
841 let mut data = msgpack.to_vec();
842 data.extend_from_slice(&nonce.to_be_bytes());
843 match vault_address {
844 None => data.push(0u8),
845 Some(addr) => {
846 data.push(1u8);
847 let addr_bytes = hex::decode(addr.trim_start_matches("0x"))
848 .map_err(|e| format!("invalid vault address: {}", e))?;
849 data.extend_from_slice(&addr_bytes);
850 }
851 }
852
853 let connection_id = keccak256(&data);
854 let agent_type_hash = keccak256(b"Agent(string source,bytes32 connectionId)");
855 let source_hash = keccak256(source.as_bytes());
856 let mut struct_data = [0u8; 96];
857 struct_data[..32].copy_from_slice(&agent_type_hash);
858 struct_data[32..64].copy_from_slice(&source_hash);
859 struct_data[64..96].copy_from_slice(&connection_id);
860 let struct_hash = keccak256(&struct_data);
861
862 let domain_sep = hyperliquid_domain_separator();
863 let mut final_data = Vec::with_capacity(66);
864 final_data.extend_from_slice(b"\x19\x01");
865 final_data.extend_from_slice(&domain_sep);
866 final_data.extend_from_slice(&struct_hash);
867 Ok(keccak256(&final_data))
868}
869
870fn format_ecdsa_signature(sig: &EcdsaSignature) -> (String, String, u8) {
872 let r = format!("0x{}", hex::encode(sig.r));
873 let s = format!("0x{}", hex::encode(sig.s));
874 let v = 27u8 + sig.v;
875 (r, s, v)
876}
877
878fn sign_digest_with_key(
880 digest: &[u8; 32],
881 private_key: &str,
882) -> Result<(String, String, u8), String> {
883 use k256::ecdsa::SigningKey;
884
885 let key_bytes = hex::decode(private_key.trim_start_matches("0x"))
886 .map_err(|e| format!("invalid private key: {}", e))?;
887 let signing_key =
888 SigningKey::from_bytes(key_bytes.as_slice().into()).map_err(|e| e.to_string())?;
889 let (sig, recovery_id) = signing_key
890 .sign_prehash_recoverable(digest)
891 .map_err(|e| e.to_string())?;
892
893 let sig_bytes = sig.to_bytes();
894 let r = format!("0x{}", hex::encode(&sig_bytes[..32]));
895 let s = format!("0x{}", hex::encode(&sig_bytes[32..64]));
896 let v = 27u8 + recovery_id.to_byte();
897
898 Ok((r, s, v))
899}
900
901async fn sign_with_msgpack(
904 msgpack: &[u8],
905 private_key: Option<&str>,
906 external_signer: Option<&Arc<dyn ExternalSigner>>,
907 nonce: u64,
908 vault_address: Option<&str>,
909 source: &str,
910) -> Result<(String, String, u8), String> {
911 let digest = compute_eip712_digest(msgpack, nonce, vault_address, source)?;
912
913 if let Some(signer) = external_signer {
914 let sig = signer.sign_prehash(&digest).await?;
915 return Ok(format_ecdsa_signature(&sig));
916 }
917
918 let key = private_key.ok_or_else(|| {
919 "no signing method available: provide private_key or external_signer".to_string()
920 })?;
921 sign_digest_with_key(&digest, key)
922}
923
924async fn sign_action(
928 private_key: Option<&str>,
929 external_signer: Option<&Arc<dyn ExternalSigner>>,
930 action: &Value,
931 vault_address: Option<&str>,
932 nonce: u64,
933 source: &str,
934) -> Result<(String, String, u8), String> {
935 let msgpack_bytes = action_to_canonical_msgpack(action)?;
936 let digest = compute_eip712_digest(&msgpack_bytes, nonce, vault_address, source)?;
937
938 if let Some(signer) = external_signer {
939 let sig = signer.sign_prehash(&digest).await?;
940 return Ok(format_ecdsa_signature(&sig));
941 }
942
943 let key = private_key.ok_or_else(|| {
944 "no signing method available: provide private_key or external_signer".to_string()
945 })?;
946 sign_digest_with_key(&digest, key)
947}
948
949#[async_trait]
952impl guilder_abstraction::TestServer for HyperliquidClient {
953 async fn ping(&self) -> Result<bool, String> {
955 self.info_post(serde_json::json!({"type": "allMids"}), 2, "ping")
957 .await
958 .map(|r| r.status().is_success())
959 }
960
961 async fn get_server_time(&self) -> Result<i64, String> {
963 Ok(std::time::SystemTime::now()
964 .duration_since(std::time::UNIX_EPOCH)
965 .map(|d| d.as_millis() as i64)
966 .unwrap_or(0))
967 }
968}
969
970#[async_trait]
971impl guilder_abstraction::GetMarketData for HyperliquidClient {
972 async fn get_symbol(&self) -> Result<Vec<String>, String> {
974 let resp = self
976 .info_post(serde_json::json!({"type": "meta"}), 20, "get_symbol")
977 .await?;
978 parse_response::<MetaResponse>(resp)
979 .await
980 .map(|r| r.universe.into_iter().map(|a| a.name).collect())
981 }
982
983 async fn get_open_interest(&self, symbol: String) -> Result<Decimal, String> {
985 let resp = self
987 .info_post(
988 serde_json::json!({"type": "metaAndAssetCtxs"}),
989 20,
990 "get_open_interest",
991 )
992 .await?;
993 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
994 .await?
995 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
996 meta.universe
997 .iter()
998 .position(|a| a.name == symbol)
999 .and_then(|i| ctxs.get(i))
1000 .and_then(|ctx| parse_decimal(&ctx.open_interest))
1001 .ok_or_else(|| format!("symbol {} not found", symbol))
1002 }
1003
1004 async fn get_asset_context(&self, symbol: String) -> Result<AssetContext, String> {
1006 let resp = self
1008 .info_post(
1009 serde_json::json!({"type": "metaAndAssetCtxs"}),
1010 20,
1011 "get_asset_context",
1012 )
1013 .await?;
1014 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
1015 .await?
1016 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
1017 let idx = meta
1018 .universe
1019 .iter()
1020 .position(|a| a.name == symbol)
1021 .ok_or_else(|| format!("symbol {} not found", symbol))?;
1022 let ctx = ctxs
1023 .get(idx)
1024 .ok_or_else(|| format!("symbol {} not found", symbol))?;
1025 Ok(AssetContext {
1026 symbol,
1027 open_interest: parse_decimal(&ctx.open_interest).ok_or("invalid open_interest")?,
1028 funding_rate: parse_decimal(&ctx.funding).ok_or("invalid funding")?,
1029 mark_price: parse_decimal(&ctx.mark_px).ok_or("invalid mark_px")?,
1030 day_volume: parse_decimal(&ctx.day_ntl_vlm).ok_or("invalid day_ntl_vlm")?,
1031 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
1032 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
1033 premium: ctx.premium.as_deref().and_then(parse_decimal),
1034 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
1035 sz_decimals: meta.universe.get(idx).map(|a| a.sz_decimals).unwrap_or(0),
1036 })
1037 }
1038
1039 async fn get_all_asset_contexts(&self) -> Result<Vec<AssetContext>, String> {
1042 let resp = self
1044 .info_post(
1045 serde_json::json!({"type": "metaAndAssetCtxs"}),
1046 20,
1047 "get_all_asset_contexts",
1048 )
1049 .await?;
1050 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
1051 .await?
1052 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
1053 let mut result = Vec::with_capacity(meta.universe.len());
1054 for (asset, ctx) in meta.universe.iter().zip(ctxs.iter()) {
1055 let Some(open_interest) = parse_decimal(&ctx.open_interest) else {
1056 continue;
1057 };
1058 let Some(funding_rate) = parse_decimal(&ctx.funding) else {
1059 continue;
1060 };
1061 let Some(mark_price) = parse_decimal(&ctx.mark_px) else {
1062 continue;
1063 };
1064 let Some(day_volume) = parse_decimal(&ctx.day_ntl_vlm) else {
1065 continue;
1066 };
1067 result.push(AssetContext {
1068 symbol: asset.name.clone(),
1069 open_interest,
1070 funding_rate,
1071 mark_price,
1072 day_volume,
1073 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
1074 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
1075 premium: ctx.premium.as_deref().and_then(parse_decimal),
1076 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
1077 sz_decimals: asset.sz_decimals,
1078 });
1079 }
1080 Ok(result)
1081 }
1082
1083 async fn get_sz_decimals(&self, symbol: String) -> Result<i32, String> {
1085 let all = self.get_all_sz_decimals().await?;
1086 all.get(&symbol)
1087 .copied()
1088 .ok_or_else(|| format!("symbol {} not found", symbol))
1089 }
1090
1091 async fn get_all_sz_decimals(&self) -> Result<HashMap<String, i32>, String> {
1093 let resp = self
1095 .info_post(
1096 serde_json::json!({"type": "metaAndAssetCtxs"}),
1097 20,
1098 "get_all_sz_decimals",
1099 )
1100 .await?;
1101 let (meta, _) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
1102 .await?
1103 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
1104 Ok(meta
1105 .universe
1106 .into_iter()
1107 .map(|a| (a.name, a.sz_decimals))
1108 .collect())
1109 }
1110
1111 async fn get_l2_orderbook(&self, symbol: String) -> Result<L2Snapshot, String> {
1113 let resp = self
1115 .info_post(
1116 serde_json::json!({"type": "l2Book", "coin": symbol}),
1117 2,
1118 "get_l2_orderbook",
1119 )
1120 .await?;
1121 let book: Option<HyperliquidWsBook> = parse_response(resp).await?;
1122 let book = match book {
1123 Some(b) => b,
1124 None => {
1125 return Ok(L2Snapshot {
1126 symbol,
1127 bids: vec![],
1128 asks: vec![],
1129 sequence: 0,
1130 })
1131 }
1132 };
1133 let bids = book
1134 .levels
1135 .first()
1136 .into_iter()
1137 .flatten()
1138 .filter_map(|level| {
1139 Some(L2Level {
1140 price: parse_decimal(&level.px)?,
1141 volume: parse_decimal(&level.sz)?,
1142 })
1143 })
1144 .collect();
1145 let asks = book
1146 .levels
1147 .get(1)
1148 .into_iter()
1149 .flatten()
1150 .filter_map(|level| {
1151 Some(L2Level {
1152 price: parse_decimal(&level.px)?,
1153 volume: parse_decimal(&level.sz)?,
1154 })
1155 })
1156 .collect();
1157 Ok(L2Snapshot {
1158 symbol: book.coin,
1159 bids,
1160 asks,
1161 sequence: book.time,
1162 })
1163 }
1164
1165 async fn get_price(&self, symbol: String) -> Result<Decimal, String> {
1167 let resp = self
1169 .info_post(serde_json::json!({"type": "allMids"}), 2, "get_price")
1170 .await?;
1171 parse_response::<HashMap<String, String>>(resp)
1172 .await?
1173 .get(&symbol)
1174 .and_then(|s| parse_decimal(s))
1175 .ok_or_else(|| format!("symbol {} not found", symbol))
1176 }
1177
1178 async fn get_predicted_fundings(&self) -> Result<Vec<PredictedFunding>, String> {
1181 let resp = self
1183 .info_post(
1184 serde_json::json!({"type": "predictedFundings"}),
1185 20,
1186 "get_predicted_fundings",
1187 )
1188 .await?;
1189 let data: PredictedFundingsResponse = parse_response(resp).await?;
1190 let mut result = Vec::new();
1191 for (symbol, venues) in data {
1192 for (venue, entry) in venues {
1193 let Some(entry) = entry else { continue };
1194 if let Some(funding_rate) = parse_decimal(&entry.funding_rate) {
1195 result.push(PredictedFunding {
1196 symbol: symbol.clone(),
1197 venue,
1198 funding_rate,
1199 next_funding_time_ms: entry.next_funding_time,
1200 });
1201 }
1202 }
1203 }
1204 Ok(result)
1205 }
1206}
1207
1208#[async_trait]
1209impl guilder_abstraction::ManageOrder for HyperliquidClient {
1210 async fn place_order(
1221 &self,
1222 symbol: String,
1223 side: OrderSide,
1224 price: Decimal,
1225 volume: Decimal,
1226 order_type: OrderType,
1227 time_in_force: TimeInForce,
1228 trigger_price: Option<Decimal>,
1229 reduce_only: bool,
1230 cloid: Option<String>,
1231 ) -> Result<OrderPlacement, String> {
1232 let asset_idx = self.get_asset_index(&symbol).await?;
1234 let is_buy = matches!(side, OrderSide::Buy);
1235
1236 let tif_str = match time_in_force {
1237 TimeInForce::Gtc => "Gtc",
1238 TimeInForce::Ioc => "Ioc",
1239 TimeInForce::Fok => "Fok",
1240 TimeInForce::Alo => "Alo",
1241 };
1242
1243 let cloid_hex = cloid.clone();
1244
1245 let is_trigger = matches!(order_type, OrderType::TakeProfit | OrderType::StopLoss);
1247
1248 let (order_msgpack, order_type_json) = if is_trigger {
1249 let trigger_px = trigger_price
1251 .ok_or_else(|| format!("{:?} order requires trigger_price to be set", order_type))?
1252 .normalize()
1253 .to_string();
1254
1255 let tpsl = match order_type {
1256 OrderType::TakeProfit => "tp",
1257 OrderType::StopLoss => "sl",
1258 _ => unreachable!(),
1259 };
1260
1261 let is_market = matches!(time_in_force, TimeInForce::Ioc);
1264
1265 let price_str = if is_market {
1268 trigger_px.clone()
1269 } else {
1270 price.normalize().to_string()
1271 };
1272
1273 let msgpack = build_trigger_order_msgpack(
1274 asset_idx,
1275 is_buy,
1276 &price_str,
1277 &volume.normalize().to_string(),
1278 reduce_only,
1279 &trigger_px,
1280 is_market,
1281 tpsl,
1282 cloid_hex.as_deref(),
1283 );
1284
1285 let json_type = if is_market {
1286 format!(
1287 r#"{{"trigger":{{"isMarket":true,"triggerPx":"{}","tpsl":"{}"}}}}"#,
1288 trigger_px, tpsl
1289 )
1290 } else {
1291 format!(
1292 r#"{{"trigger":{{"isMarket":false,"triggerPx":"{}","tpsl":"{}"}}}}"#,
1293 trigger_px, tpsl
1294 )
1295 };
1296
1297 (msgpack, json_type)
1298 } else {
1299 let (order_kind, tif_bytes) = match order_type {
1301 OrderType::Limit => ("limit", tif_str.as_bytes()),
1302 OrderType::Market => ("limit", b"Ioc".as_slice()),
1303 _ => unreachable!(),
1304 };
1305
1306 let price_str = price.normalize().to_string();
1307 let size_str = volume.normalize().to_string();
1308
1309 let msgpack = build_order_msgpack(
1310 asset_idx,
1311 is_buy,
1312 &price_str,
1313 &size_str,
1314 reduce_only,
1315 order_kind,
1316 tif_bytes,
1317 cloid_hex.as_deref(),
1318 );
1319
1320 let json_type = match order_type {
1321 OrderType::Limit => format!(r#"{{"limit":{{"tif":"{tif_str}"}}}}"#),
1322 OrderType::Market => r#"{"limit":{"tif":"Ioc"}}"#.to_string(),
1323 _ => unreachable!(),
1324 };
1325
1326 (msgpack, json_type)
1327 };
1328
1329 let mut action_msgpack = Vec::new();
1332 action_msgpack.push(0x83); action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("type".to_string())));
1334 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("order".to_string())));
1335 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("orders".to_string())));
1336 action_msgpack.push(0x91); action_msgpack.extend_from_slice(&order_msgpack);
1338 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("grouping".to_string())));
1339 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("na".to_string())));
1340
1341 let cloid_json = if let Some(ref c) = cloid_hex {
1342 format!(r#","c":"{c}""#)
1343 } else {
1344 String::new()
1345 };
1346
1347 let reduce_json = if reduce_only { "true" } else { "false" };
1348
1349 let price_for_json = if is_trigger && matches!(time_in_force, TimeInForce::Ioc) {
1350 if let Some(ref tp) = trigger_price {
1352 tp.normalize().to_string()
1353 } else {
1354 price.normalize().to_string()
1355 }
1356 } else {
1357 price.normalize().to_string()
1358 };
1359
1360 let action_json_str = format!(
1361 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"}}"#,
1362 price = price_for_json,
1363 size = volume.normalize().to_string(),
1364 );
1365
1366 let private_key = self.require_private_key()?;
1368 let nonce = std::time::SystemTime::now()
1369 .duration_since(std::time::UNIX_EPOCH)
1370 .unwrap()
1371 .as_millis() as u64;
1372
1373 let (r, s, v) = sign_with_msgpack(
1374 &action_msgpack,
1375 private_key,
1376 self.external_signer.as_ref(),
1377 nonce,
1378 None,
1379 self.network.eip712_source(),
1380 )
1381 .await?;
1382
1383 let payload_str = format!(
1384 r#"{{"action":{},"nonce":{},"signature":{{"r":"{}","s":"{}","v":{}}},"vaultAddress":null,"expiresAfter":null}}"#,
1385 action_json_str, nonce, r, s, v
1386 );
1387
1388 self.rest_limiter.acquire(1).await.map_err(|e| {
1389 format!(
1390 "rate_limited: rest_weight exhausted, retry_after_ms={}",
1391 e.retry_after.as_millis()
1392 )
1393 })?;
1394 self.address_limiter.acquire(1, false).await.map_err(|e| {
1395 format!(
1396 "rate_limited: address quota exhausted, retry_after_ms={}",
1397 e.retry_after.as_millis()
1398 )
1399 })?;
1400
1401 let resp = self
1402 .client
1403 .post(self.network.exchange_url())
1404 .header("Content-Type", "application/json")
1405 .body(payload_str)
1406 .send()
1407 .await
1408 .map_err(|e| e.to_string())?;
1409
1410 let status = resp.status();
1411 if !status.is_success() {
1412 let text = resp.text().await.map_err(|e| e.to_string())?;
1413 return Err(format!("HTTP {status}: {text}"));
1414 }
1415
1416 let body: Value = parse_response(resp).await?;
1417 if body["status"].as_str() == Some("err") {
1418 return Err(body["response"]
1419 .as_str()
1420 .unwrap_or("unknown error")
1421 .to_string());
1422 }
1423 let statuses = &body["response"]["data"]["statuses"][0];
1424
1425 let (oid, returned_cloid, timestamp_ms) = if let Some(resting) = statuses.get("resting") {
1426 let oid = resting["oid"]
1427 .as_i64()
1428 .ok_or_else(|| format!("resting status missing oid: {}", body))?;
1429 let returned_cloid = resting["cloid"].as_str().map(|s: &str| s.to_string());
1430 let ts = std::time::SystemTime::now()
1432 .duration_since(std::time::UNIX_EPOCH)
1433 .unwrap()
1434 .as_millis() as i64;
1435 (oid, returned_cloid, ts)
1436 } else if let Some(filled) = statuses.get("filled") {
1437 let oid = filled["oid"]
1438 .as_i64()
1439 .ok_or_else(|| format!("filled status missing oid: {}", body))?;
1440 let ts = std::time::SystemTime::now()
1442 .duration_since(std::time::UNIX_EPOCH)
1443 .unwrap()
1444 .as_millis() as i64;
1445 (oid, None, ts)
1446 } else if let Some(error) = statuses.get("error") {
1447 return Err(error
1448 .as_str()
1449 .unwrap_or("order rejected with unknown error")
1450 .to_string());
1451 } else {
1452 return Err(format!("unexpected order status: {}", body));
1453 };
1454
1455 Ok(OrderPlacement {
1456 order_id: oid,
1457 symbol,
1458 side,
1459 price,
1460 quantity: volume,
1461 timestamp_ms,
1462 cloid: returned_cloid.or(cloid),
1463 order_type,
1464 trigger_price,
1465 reduce_only,
1466 })
1467 }
1468
1469 async fn change_order_by_cloid(
1472 &self,
1473 cloid: i64,
1474 price: Decimal,
1475 volume: Decimal,
1476 ) -> Result<i64, String> {
1477 let user = self.require_user_address()?;
1478
1479 let resp = self
1481 .info_post(
1482 serde_json::json!({"type": "openOrders", "user": user}),
1483 20,
1484 "change_order_by_cloid",
1485 )
1486 .await?;
1487 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1488 let order = orders
1489 .iter()
1490 .find(|o| o.oid == cloid)
1491 .ok_or_else(|| format!("order {} not found", cloid))?;
1492
1493 let asset_idx = self.get_asset_index(&order.coin).await?;
1494 let is_buy = order.side == "B";
1495
1496 let action = serde_json::json!({
1497 "type": "batchModify",
1498 "modifies": [{
1499 "oid": cloid,
1500 "order": {
1501 "a": asset_idx,
1502 "b": is_buy,
1503 "p": price.to_string(),
1504 "s": volume.to_string(),
1505 "r": false,
1506 "t": {"limit": {"tif": "Gtc"}}
1507 }
1508 }]
1509 });
1510
1511 self.submit_signed_action(action, None).await?;
1512 Ok(cloid)
1513 }
1514
1515 async fn cancel_order_by_cloid(&self, cloid: String) -> Result<(), String> {
1520 let user = self.require_user_address()?;
1521
1522 let resp = self
1524 .info_post(
1525 serde_json::json!({"type": "openOrders", "user": user}),
1526 20,
1527 "cancel_order_by_cloid",
1528 )
1529 .await?;
1530 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1531 let order = orders
1532 .iter()
1533 .find(|o| o.cloid.as_ref() == Some(&cloid))
1534 .ok_or_else(|| format!("order with cloid {} not found", cloid))?;
1535
1536 let meta_resp = self
1538 .info_post(
1539 serde_json::json!({"type": "meta"}),
1540 20,
1541 "cancel_order_by_cloid",
1542 )
1543 .await?;
1544 let meta: MetaResponse = parse_response(meta_resp).await?;
1545
1546 let asset_idx = meta
1547 .universe
1548 .iter()
1549 .position(|a| a.name == order.coin)
1550 .ok_or_else(|| format!("asset {} not found in meta", order.coin))?;
1551
1552 let action = serde_json::json!({
1555 "type": "cancel",
1556 "cancels": [{"a": asset_idx, "o": order.oid}]
1557 });
1558
1559 self.submit_signed_action(action, None).await?;
1560 Ok(())
1561 }
1562
1563 async fn cancel_all_order(&self) -> Result<bool, String> {
1566 let user = self.require_user_address()?;
1567
1568 let resp = self
1570 .info_post(
1571 serde_json::json!({"type": "openOrders", "user": user}),
1572 20,
1573 "cancel_all_order",
1574 )
1575 .await?;
1576 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1577 if orders.is_empty() {
1578 return Ok(true);
1579 }
1580
1581 let meta_resp = self
1583 .info_post(serde_json::json!({"type": "meta"}), 20, "cancel_all_order")
1584 .await?;
1585 let meta: MetaResponse = parse_response(meta_resp).await?;
1586
1587 let cancels: Vec<Value> = orders
1588 .iter()
1589 .filter_map(|o| {
1590 let asset_idx = meta.universe.iter().position(|a| a.name == o.coin)?;
1591 Some(serde_json::json!({"a": asset_idx, "o": o.oid}))
1592 })
1593 .collect();
1594
1595 let action = serde_json::json!({"type": "cancel", "cancels": cancels});
1596 self.submit_signed_action(action, None).await?;
1597 Ok(true)
1598 }
1599}
1600
1601#[async_trait]
1602impl guilder_abstraction::SubscribeMarketData for HyperliquidClient {
1603 fn subscribe_l2_update(&self, symbol: String) -> BoxStream<Result<L2Update, String>> {
1604 Box::pin(stream::iter(vec![Err(format!(
1605 "subscribe_l2_update is unsupported for {symbol}; use subscribe_l2_snapshot"
1606 ))]))
1607 }
1608
1609 fn subscribe_l2_snapshot(&self, symbol: String) -> BoxStream<Result<L2Snapshot, String>> {
1610 let subscription = HyperliquidSubscription::L2Book { coin: symbol };
1611 Box::pin(managed_stream(
1612 self.market_ws_manager.clone(),
1613 subscription,
1614 |msg: HyperliquidWsInboundMessage| {
1615 if let Some(snapshot) = msg.as_l2_snapshot() {
1616 vec![Ok(snapshot)]
1617 } else {
1618 vec![]
1619 }
1620 },
1621 ))
1622 }
1623
1624 fn subscribe_asset_context(&self, symbol: String) -> BoxStream<Result<AssetContext, String>> {
1625 let subscription = HyperliquidSubscription::ActiveAssetCtx { coin: symbol };
1626 Box::pin(managed_stream(
1627 self.market_ws_manager.clone(),
1628 subscription,
1629 |msg: HyperliquidWsInboundMessage| {
1630 if let Some(ctx) = msg.as_asset_context() {
1631 vec![Ok(ctx)]
1632 } else {
1633 vec![]
1634 }
1635 },
1636 ))
1637 }
1638
1639 fn subscribe_liquidation(&self, user: String) -> BoxStream<Result<Liquidation, String>> {
1640 subscribe_user_stream(
1641 self,
1642 user.clone(),
1643 HyperliquidSubscription::UserEvents { user_addr: user },
1644 |msg: HyperliquidWsInboundMessage| {
1645 if let Some(liq) = msg.as_liquidation() {
1646 vec![Ok(liq)]
1647 } else {
1648 vec![]
1649 }
1650 },
1651 )
1652 }
1653
1654 fn subscribe_fill(&self, symbol: String) -> BoxStream<Result<Fill, String>> {
1655 let subscription = HyperliquidSubscription::Trades { coin: symbol };
1656 Box::pin(managed_stream(
1657 self.market_ws_manager.clone(),
1658 subscription,
1659 |msg: HyperliquidWsInboundMessage| {
1660 if let Some(fills) = msg.as_trades() {
1661 fills.into_iter().map(Ok).collect()
1662 } else {
1663 vec![]
1664 }
1665 },
1666 ))
1667 }
1668
1669 async fn unsubscribe_all(&self) {
1672 self.market_ws_manager.shutdown();
1673 }
1674}
1675
1676fn subscribe_user_stream<T, F>(
1677 client: &HyperliquidClient,
1678 user_addr: String,
1679 subscription: HyperliquidSubscription,
1680 parse: F,
1681) -> BoxStream<Result<T, String>>
1682where
1683 T: Send + 'static,
1684 F: Fn(HyperliquidWsInboundMessage) -> Vec<Result<T, String>> + Send + Sync + 'static,
1685{
1686 let manager = get_or_create_user_manager(
1687 &client.user_ws_managers,
1688 client.ws_send_limiter.clone(),
1689 user_addr,
1690 client.network.ws_url(),
1691 );
1692 Box::pin(async_stream::stream! {
1693 let stream = managed_stream(manager, subscription, parse);
1694 tokio::pin!(stream);
1695 while let Some(item) = stream.next().await {
1696 yield item;
1697 }
1698 })
1699}
1700
1701fn get_or_create_user_manager(
1702 user_ws_managers: &RwLock<HashMap<String, HyperliquidWsManager>>,
1703 ws_send_limiter: WsSendRateLimiter,
1704 user_addr: String,
1705 ws_url: &'static str,
1706) -> HyperliquidWsManager {
1707 {
1708 let managers = user_ws_managers.read().unwrap_or_else(|e| e.into_inner());
1709 if let Some(manager) = managers.get(&user_addr) {
1710 return manager.clone();
1711 }
1712 }
1713
1714 let mut managers = user_ws_managers.write().unwrap_or_else(|e| e.into_inner());
1715 managers
1716 .entry(user_addr.clone())
1717 .or_insert_with(|| HyperliquidWsManager::new(Some(user_addr), ws_send_limiter, ws_url))
1718 .clone()
1719}
1720
1721#[async_trait]
1722impl guilder_abstraction::GetAccountSnapshot for HyperliquidClient {
1723 async fn get_positions(&self) -> Result<Vec<Position>, String> {
1726 let user = self.require_user_address()?;
1727 let resp = self
1729 .info_post(
1730 serde_json::json!({"type": "clearinghouseState", "user": user}),
1731 2,
1732 "get_positions",
1733 )
1734 .await?;
1735 let state: ClearinghouseStateResponse = parse_response(resp).await?;
1736
1737 Ok(state
1738 .asset_positions
1739 .into_iter()
1740 .filter_map(|ap| {
1741 let p = ap.position;
1742 let size = parse_decimal(&p.szi)?;
1743 if size.is_zero() {
1744 return None;
1745 }
1746 let entry_price = p
1751 .entry_px
1752 .as_deref()
1753 .and_then(parse_decimal);
1754 let Some(entry_price) = entry_price else {
1755 return None;
1756 };
1757 let side = if size > Decimal::ZERO {
1758 OrderSide::Buy
1759 } else {
1760 OrderSide::Sell
1761 };
1762 Some(Position {
1763 symbol: p.coin,
1764 side,
1765 size: size.abs(),
1766 entry_price,
1767 })
1768 })
1769 .collect())
1770 }
1771
1772 async fn get_open_orders(&self) -> Result<Vec<OpenOrder>, String> {
1775 let user = self.require_user_address()?;
1776 let resp = self
1778 .info_post(
1779 serde_json::json!({"type": "openOrders", "user": user}),
1780 20,
1781 "get_open_orders",
1782 )
1783 .await?;
1784 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1785
1786 Ok(orders
1787 .into_iter()
1788 .filter_map(|o| {
1789 let price = parse_decimal(&o.limit_px)?;
1790 let quantity = parse_decimal(&o.orig_sz)?;
1791 let remaining = parse_decimal(&o.sz)?;
1792 let filled_quantity = quantity - remaining;
1793 let side = if o.side == "B" {
1794 OrderSide::Buy
1795 } else {
1796 OrderSide::Sell
1797 };
1798 Some(OpenOrder {
1799 order_id: o.oid,
1800 symbol: o.coin,
1801 side,
1802 price,
1803 quantity,
1804 filled_quantity,
1805 order_type: None, trigger_price: None, reduce_only: false, })
1809 })
1810 .collect())
1811 }
1812
1813 async fn get_balance(&self) -> Result<Vec<guilder_abstraction::AccountBalance>, String> {
1817 let user = self.require_user_address()?;
1818 let resp = self
1820 .info_post(
1821 serde_json::json!({"type": "spotClearinghouseState", "user": user}),
1822 15,
1823 "get_balance",
1824 )
1825 .await?;
1826
1827 #[allow(dead_code)]
1828 #[derive(Deserialize)]
1829 struct SpotBalance {
1830 coin: String,
1831 total: String,
1832 hold: String,
1833 #[serde(default)]
1834 token: Option<i32>,
1835 #[serde(default)]
1836 #[serde(rename = "entryNtl")]
1837 entry_ntl: Option<String>,
1838 }
1839
1840 let state: SpotStateResponse = parse_response(resp).await?;
1841
1842 let perp_parsed: Option<ClearinghouseStateResponse> = match self
1844 .info_post(
1845 serde_json::json!({"type": "clearinghouseState", "user": user}),
1846 2,
1847 "get_balance_margin",
1848 )
1849 .await
1850 {
1851 Ok(resp) => parse_response::<ClearinghouseStateResponse>(resp).await.ok(),
1852 Err(_) => None,
1853 };
1854
1855 let perp_margin_used: Option<Decimal> = perp_parsed.as_ref().and_then(|ch| {
1858 ch.margin_summary
1859 .total_margin_used
1860 .as_deref()
1861 .and_then(parse_decimal)
1862 });
1863
1864 let mut rows = map_spot_state(state, perp_margin_used)?;
1867
1868 let perp_state: ClearinghouseStateResponse = match perp_parsed {
1874 Some(ch) => ch,
1875 None => {
1876 let resp = self
1879 .info_post(
1880 serde_json::json!({"type": "clearinghouseState", "user": user}),
1881 2,
1882 "get_balance_perp_row",
1883 )
1884 .await?;
1885 parse_response::<ClearinghouseStateResponse>(resp).await?
1886 }
1887 };
1888 rows.push(map_perp_state(perp_state)?);
1889
1890 Ok(rows)
1891 }
1892
1893 async fn get_user_rate_limit(&self) -> Result<guilder_abstraction::UserRateLimit, String> {
1896 let user = self.require_user_address()?;
1897 let resp = self
1898 .info_post(
1899 serde_json::json!({"type": "userRateLimit", "user": user}),
1900 20,
1901 "get_user_rate_limit",
1902 )
1903 .await?;
1904 let val = parse_response::<Value>(resp).await?;
1905
1906 let cumulative_volume = val["cumVlm"]
1907 .as_str()
1908 .and_then(parse_decimal)
1909 .ok_or_else(|| "missing or invalid cumVlm".to_string())?;
1910 let requests_used = val["nRequestsUsed"]
1911 .as_i64()
1912 .ok_or_else(|| "missing or invalid nRequestsUsed".to_string())?;
1913 let requests_cap = val["nRequestsCap"]
1914 .as_i64()
1915 .ok_or_else(|| "missing or invalid nRequestsCap".to_string())?;
1916 let requests_surplus = val["nRequestsSurplus"]
1917 .as_i64()
1918 .ok_or_else(|| "missing or invalid nRequestsSurplus".to_string())?;
1919
1920 Ok(guilder_abstraction::UserRateLimit {
1921 cumulative_volume,
1922 requests_used,
1923 requests_cap,
1924 requests_surplus,
1925 })
1926 }
1927}
1928
1929#[async_trait]
1930impl guilder_abstraction::SubscribeUserEvents for HyperliquidClient {
1931 fn subscribe_user_fills(&self) -> BoxStream<Result<UserFill, String>> {
1932 let Some(addr) = self.user_address.as_ref() else {
1933 return Box::pin(stream::iter(vec![Err(
1934 "user address not registered".to_string()
1935 )]));
1936 };
1937 subscribe_user_stream(
1938 self,
1939 addr.clone(),
1940 HyperliquidSubscription::UserEvents {
1941 user_addr: addr.clone(),
1942 },
1943 |msg: HyperliquidWsInboundMessage| {
1944 if let Some(fills) = msg.as_user_fills() {
1945 fills.into_iter().map(Ok).collect()
1946 } else {
1947 vec![]
1948 }
1949 },
1950 )
1951 }
1952
1953 fn subscribe_order_updates(&self) -> BoxStream<Result<OrderUpdate, String>> {
1954 let Some(addr) = self.user_address.as_ref() else {
1955 return Box::pin(stream::iter(vec![Err(
1956 "user address not registered".to_string()
1957 )]));
1958 };
1959 subscribe_user_stream(
1960 self,
1961 addr.clone(),
1962 HyperliquidSubscription::OrderUpdates {
1963 user_addr: addr.clone(),
1964 },
1965 |msg: HyperliquidWsInboundMessage| {
1966 if let Some(updates) = msg.as_order_updates() {
1967 updates.into_iter().map(Ok).collect()
1968 } else {
1969 vec![]
1970 }
1971 },
1972 )
1973 }
1974
1975 fn subscribe_funding_payments(&self) -> BoxStream<Result<FundingPayment, String>> {
1976 let Some(addr) = self.user_address.as_ref() else {
1977 return Box::pin(stream::iter(vec![Err(
1978 "user address not registered".to_string()
1979 )]));
1980 };
1981 subscribe_user_stream(
1982 self,
1983 addr.clone(),
1984 HyperliquidSubscription::UserEvents {
1985 user_addr: addr.clone(),
1986 },
1987 |msg: HyperliquidWsInboundMessage| {
1988 if let Some(p) = msg.as_funding_payment() {
1989 vec![Ok(p)]
1990 } else {
1991 vec![]
1992 }
1993 },
1994 )
1995 }
1996
1997 fn subscribe_deposits(&self) -> BoxStream<Result<Deposit, String>> {
1998 let Some(addr) = self.user_address.as_ref() else {
1999 return Box::pin(stream::iter(vec![Err(
2000 "user address not registered".to_string()
2001 )]));
2002 };
2003 subscribe_user_stream(
2004 self,
2005 addr.clone(),
2006 HyperliquidSubscription::NonFundingLedger {
2007 user_addr: addr.clone(),
2008 },
2009 |msg: HyperliquidWsInboundMessage| {
2010 if let Some(deps) = msg.as_deposits() {
2011 deps.into_iter().map(Ok).collect()
2012 } else {
2013 vec![]
2014 }
2015 },
2016 )
2017 }
2018
2019 fn subscribe_withdrawals(&self) -> BoxStream<Result<Withdrawal, String>> {
2020 let Some(addr) = self.user_address.as_ref() else {
2021 return Box::pin(stream::iter(vec![Err(
2022 "user address not registered".to_string()
2023 )]));
2024 };
2025 subscribe_user_stream(
2026 self,
2027 addr.clone(),
2028 HyperliquidSubscription::NonFundingLedger {
2029 user_addr: addr.clone(),
2030 },
2031 |msg: HyperliquidWsInboundMessage| {
2032 if let Some(wds) = msg.as_withdrawals() {
2033 wds.into_iter().map(Ok).collect()
2034 } else {
2035 vec![]
2036 }
2037 },
2038 )
2039 }
2040
2041 fn subscribe_spot_balance(
2043 &self,
2044 ) -> BoxStream<Result<Vec<guilder_abstraction::AccountBalance>, String>> {
2045 let Some(addr) = self.user_address.as_ref() else {
2046 return Box::pin(stream::iter(vec![Err(
2047 "user address not registered".to_string()
2048 )]));
2049 };
2050 self.subscribe_spot_balance_with_address(addr.clone())
2051 }
2052
2053 fn subscribe_spot_balance_with_address(
2055 &self,
2056 address: String,
2057 ) -> BoxStream<Result<Vec<guilder_abstraction::AccountBalance>, String>> {
2058 subscribe_user_stream(
2059 self,
2060 address.clone(),
2061 HyperliquidSubscription::UserEvents { user_addr: address },
2062 |msg: HyperliquidWsInboundMessage| {
2063 if let Some(balances) = msg.as_spot_balance() {
2064 vec![Ok(balances)]
2065 } else {
2066 vec![]
2067 }
2068 },
2069 )
2070 }
2071
2072 async fn unsubscribe_user_events(&self) {
2073 if let Some(addr) = &self.user_address {
2075 self.market_ws_manager.unsubscribe_user(addr);
2076 }
2077 let managers = self
2079 .user_ws_managers
2080 .read()
2081 .unwrap_or_else(|e| e.into_inner());
2082 for (addr, manager) in managers.iter() {
2083 manager.unsubscribe_user(addr);
2084 }
2085 }
2086}
2087
2088#[async_trait]
2089impl guilder_abstraction::SubscribeMarketDataOps for HyperliquidClient {
2090 async fn unsubscribe_market_data(&self, symbol: String) {
2091 self.market_ws_manager.unsubscribe_by_coin(&symbol);
2092 }
2093}
2094
2095#[cfg(test)]
2096mod msgpack_tests {
2097 use super::*;
2098 use serde_json::json;
2099
2100 #[test]
2101 fn test_eip712_digest_known_answers() {
2102 let action = json!({
2110 "type": "order",
2111 "orders": [{
2112 "a": 1, "b": true, "p": "1500.5", "s": "0.05",
2113 "r": false, "t": {"limit": {"tif": "Gtc"}}
2114 }],
2115 "grouping": "na"
2116 });
2117 let nonce: u64 = 1758572400123;
2118 let msgpack = action_to_canonical_msgpack(&action).unwrap();
2119
2120 let mainnet = compute_eip712_digest(&msgpack, nonce, None, "a").unwrap();
2121 assert_eq!(
2122 hex::encode(mainnet),
2123 "28cd97dd515629633af463bd7edaab14e61b3941b638d410c317cac7e0aed860"
2124 );
2125
2126 let testnet = compute_eip712_digest(&msgpack, nonce, None, "b").unwrap();
2127 assert_eq!(
2128 hex::encode(testnet),
2129 "d452e53f806773ca6d7ab5d10147518bff0d8b64a4404dcb062e3d2fb3d85c0b"
2130 );
2131
2132 assert_ne!(mainnet, testnet);
2133 assert_eq!(HyperliquidNetwork::Mainnet.eip712_source(), "a");
2134 assert_eq!(HyperliquidNetwork::Testnet.eip712_source(), "b");
2135 }
2136
2137 #[test]
2138 fn test_msgpack_null() {
2139 let result = value_to_msgpack(&Value::Null);
2140 assert_eq!(result, vec![0xc0]);
2141 }
2142
2143 #[test]
2144 fn test_msgpack_bool() {
2145 assert_eq!(value_to_msgpack(&Value::Bool(true)), vec![0xc3]);
2146 assert_eq!(value_to_msgpack(&Value::Bool(false)), vec![0xc2]);
2147 }
2148
2149 #[test]
2150 fn test_msgpack_positive_fixint() {
2151 assert_eq!(value_to_msgpack(&json!(0)), vec![0x00]);
2153 assert_eq!(value_to_msgpack(&json!(1)), vec![0x01]);
2154 assert_eq!(value_to_msgpack(&json!(127)), vec![0x7f]);
2155 }
2156
2157 #[test]
2158 fn test_msgpack_uint8() {
2159 assert_eq!(value_to_msgpack(&json!(128)), vec![0xcc, 0x80]);
2161 assert_eq!(value_to_msgpack(&json!(255)), vec![0xcc, 0xff]);
2162 }
2163
2164 #[test]
2165 fn test_msgpack_uint16() {
2166 assert_eq!(value_to_msgpack(&json!(256)), vec![0xcd, 0x01, 0x00]);
2168 assert_eq!(value_to_msgpack(&json!(65535)), vec![0xcd, 0xff, 0xff]);
2169 }
2170
2171 #[test]
2172 fn test_msgpack_uint32() {
2173 assert_eq!(
2175 value_to_msgpack(&json!(65536)),
2176 vec![0xce, 0x00, 0x01, 0x00, 0x00]
2177 );
2178 assert_eq!(
2179 value_to_msgpack(&json!(4294967295u64)),
2180 vec![0xce, 0xff, 0xff, 0xff, 0xff]
2181 );
2182 }
2183
2184 #[test]
2185 fn test_msgpack_uint64() {
2186 let big: u64 = 4294967296;
2188 assert_eq!(
2189 value_to_msgpack(&json!(big)),
2190 vec![0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]
2191 );
2192 }
2193
2194 #[test]
2195 fn test_msgpack_negative_fixint() {
2196 assert_eq!(value_to_msgpack(&json!(-1)), vec![0xff]);
2198 assert_eq!(value_to_msgpack(&json!(-32)), vec![0xe0]);
2199 }
2200
2201 #[test]
2202 fn test_msgpack_int8() {
2203 assert_eq!(value_to_msgpack(&json!(-33)), vec![0xd0, 0xdf]);
2205 assert_eq!(value_to_msgpack(&json!(-128)), vec![0xd0, 0x80]);
2206 }
2207
2208 #[test]
2209 fn test_msgpack_int16() {
2210 assert_eq!(value_to_msgpack(&json!(-129)), vec![0xd1, 0xff, 0x7f]);
2212 assert_eq!(value_to_msgpack(&json!(-32768)), vec![0xd1, 0x80, 0x00]);
2213 }
2214
2215 #[test]
2216 fn test_msgpack_int32() {
2217 assert_eq!(
2219 value_to_msgpack(&json!(-32769)),
2220 vec![0xd2, 0xff, 0xff, 0x7f, 0xff]
2221 );
2222 assert_eq!(
2223 value_to_msgpack(&json!(-2147483648i64)),
2224 vec![0xd2, 0x80, 0x00, 0x00, 0x00]
2225 );
2226 }
2227
2228 #[test]
2229 fn test_msgpack_int64() {
2230 let val: i64 = -2147483649;
2231 let result = value_to_msgpack(&json!(val));
2232 assert_eq!(result[0], 0xd3); assert_eq!(result.len(), 9);
2234 }
2235
2236 #[test]
2237 fn test_msgpack_float() {
2238 let result = value_to_msgpack(&json!(3.14));
2239 assert_eq!(result[0], 0xcb); assert_eq!(result.len(), 9);
2241 }
2242
2243 #[test]
2244 fn test_msgpack_fixstr() {
2245 assert_eq!(value_to_msgpack(&json!("")), vec![0xa0]);
2247 assert_eq!(value_to_msgpack(&json!("hello")), {
2248 let mut expected = vec![0xa5];
2249 expected.extend_from_slice(b"hello");
2250 expected
2251 });
2252 let s = "a".repeat(31);
2253 let result = value_to_msgpack(&json!(s));
2254 assert_eq!(result[0], 0xbf); assert_eq!(result.len(), 32);
2256 }
2257
2258 #[test]
2259 fn test_msgpack_str8() {
2260 let s = "a".repeat(32);
2261 let result = value_to_msgpack(&json!(s));
2262 assert_eq!(result[0], 0xd9); assert_eq!(result[1], 32);
2264 assert_eq!(result.len(), 34);
2265 }
2266
2267 #[test]
2268 fn test_msgpack_fixarray() {
2269 assert_eq!(value_to_msgpack(&json!([])), vec![0x90]);
2271 let result = value_to_msgpack(&json!([1, 2, 3]));
2272 assert_eq!(result[0], 0x93);
2273 assert_eq!(result, vec![0x93, 0x01, 0x02, 0x03]);
2274 }
2275
2276 #[test]
2277 fn test_msgpack_fixmap() {
2278 assert_eq!(value_to_msgpack(&json!({})), vec![0x80]);
2280 let result = value_to_msgpack(&json!({"a": 1}));
2281 assert_eq!(result[0], 0x81); assert_eq!(result, {
2283 let mut expected = vec![0x81];
2284 expected.extend_from_slice(&value_to_msgpack(&json!("a")));
2285 expected.extend_from_slice(&value_to_msgpack(&json!(1)));
2286 expected
2287 });
2288 }
2289
2290 #[test]
2291 fn test_msgmap_preserves_insertion_order() {
2292 let val = json!({
2294 "z": 1,
2295 "a": 2,
2296 "m": 3
2297 });
2298 let result = value_to_msgpack(&val);
2299 assert_eq!(result[0], 0x83);
2301 assert_eq!(result[1], 0xa1); assert_eq!(result[2], b'z');
2304 }
2305
2306 #[test]
2307 fn test_msgpack_mixed_array() {
2308 let val = json!([null, true, false, 42, "hi", [1, 2]]);
2309 let result = value_to_msgpack(&val);
2310 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);
2317 assert_eq!(result[6], b'h');
2318 assert_eq!(result[7], b'i');
2319 }
2320
2321 #[test]
2322 fn test_build_order_msgpack_without_cloid() {
2323 let result = build_order_msgpack(
2324 0, true, "1000", "0.1", false, "limit", b"gtc", None, );
2333 assert_eq!(result[0], 0x86);
2335 }
2336
2337 #[test]
2338 fn test_build_order_msgpack_with_cloid() {
2339 let result = build_order_msgpack(
2340 0, true, "1000", "0.1", false, "limit", b"gtc", Some("my-cloid"), );
2349 assert_eq!(result[0], 0x87);
2351 }
2352
2353 #[test]
2354 fn test_action_to_canonical_msgpack() {
2355 let action = json!({
2356 "type": "order",
2357 "orders": [{"a": 0, "b": true, "p": "1000", "s": "0.1", "r": false, "t": {"limit": {"tif": "gtc"}}}],
2358 "grouping": "na"
2359 });
2360 let result = action_to_canonical_msgpack(&action).unwrap();
2361 assert_eq!(result[0], 0x83);
2363 }
2364
2365 #[test]
2366 fn test_msgpack_matches_rmp_serde_for_simple_values() {
2367 use rmp_serde::to_vec;
2369
2370 for val in [
2371 json!(0),
2372 json!(127),
2373 json!(255),
2374 json!(1000),
2375 json!(-1),
2376 json!(-32),
2377 json!(-128),
2378 ] {
2379 let ours = value_to_msgpack(&val);
2380 let theirs = to_vec(&val).unwrap();
2381 assert_eq!(
2382 ours, theirs,
2383 "mismatch for {}: ours={:?}, rmp={:?}",
2384 val, ours, theirs
2385 );
2386 }
2387 }
2388
2389 #[test]
2390 fn test_msgpack_string_encoding() {
2391 use rmp_serde::to_vec;
2392 for val in [
2393 json!(""),
2394 json!("a"),
2395 json!("hello world"),
2396 json!("BTC-USD"),
2397 ] {
2398 let ours = value_to_msgpack(&val);
2399 let theirs = to_vec(&val).unwrap();
2400 assert_eq!(
2401 ours, theirs,
2402 "mismatch for {}: ours={:?}, rmp={:?}",
2403 val, ours, theirs
2404 );
2405 }
2406 }
2407
2408 #[test]
2409 fn test_msgpack_bool_encoding() {
2410 use rmp_serde::to_vec;
2411 let theirs = to_vec(&json!(true)).unwrap();
2412 assert_eq!(value_to_msgpack(&json!(true)), theirs);
2413 let theirs = to_vec(&json!(false)).unwrap();
2414 assert_eq!(value_to_msgpack(&json!(false)), theirs);
2415 }
2416
2417 #[test]
2418 fn test_msgpack_null_encoding() {
2419 use rmp_serde::to_vec;
2420 let theirs = to_vec(&Value::Null).unwrap();
2421 assert_eq!(value_to_msgpack(&Value::Null), theirs);
2422 }
2423
2424 #[test]
2425 fn test_msgpack_nested_object() {
2426 let val = json!({
2427 "outer": {
2428 "inner": 42
2429 }
2430 });
2431 let result = value_to_msgpack(&val);
2432 assert_eq!(result[0], 0x81); }
2434
2435 #[test]
2436 fn test_msgpack_empty_containers() {
2437 assert_eq!(value_to_msgpack(&json!([])), vec![0x90]);
2438 assert_eq!(value_to_msgpack(&json!({})), vec![0x80]);
2439 }
2440}
2441
2442#[async_trait::async_trait]
2443impl guilder_abstraction::ListingEventSource for HyperliquidClient {
2444 async fn get_listing_events(&self) -> Result<Vec<guilder_abstraction::ListingEvent>, String> {
2452 let resp = self
2454 .info_post(
2455 serde_json::json!({"type": "meta"}),
2456 20,
2457 "get_listing_events",
2458 )
2459 .await?;
2460 let meta = parse_response::<MetaResponse>(resp).await?;
2461 Ok(meta
2462 .universe
2463 .into_iter()
2464 .map(|a| guilder_abstraction::ListingEvent {
2465 ticker: a.name,
2466 exchange: "hyperliquid_perp".to_string(),
2467 event: if a.is_delisted { "delist" } else { "list" }.to_string(),
2468 event_time: String::new(),
2469 })
2470 .collect())
2471 }
2472
2473 async fn get_current_universe(&self) -> Result<Vec<guilder_abstraction::SymbolStatus>, String> {
2474 let resp = self
2476 .info_post(
2477 serde_json::json!({"type": "meta"}),
2478 20,
2479 "get_current_universe",
2480 )
2481 .await?;
2482 let meta = parse_response::<MetaResponse>(resp).await?;
2483 Ok(meta
2484 .universe
2485 .into_iter()
2486 .map(|a| guilder_abstraction::SymbolStatus {
2487 ticker: a.name,
2488 exchange: "hyperliquid_perp".to_string(),
2489 is_delisted: a.is_delisted,
2490 })
2491 .collect())
2492 }
2493}
2494
2495#[cfg(test)]
2496mod spot_state_tests {
2497 use super::*;
2498
2499 const TESTNET_SHAPE: &str =
2504 r#"{"balances":[{"coin":"USDC","token":0,"total":"969.0","hold":"0.0","entryNtl":"0.0"}]}"#;
2505
2506 const MAINNET_SHAPE: &str = r#"{"balances":[{"coin":"USDC","token":0,"total":"969.0","hold":"10.0","entryNtl":"0.0"}],"tokenToAvailableAfterMaintenance":[[0,"955.0"]]}"#;
2508
2509 #[test]
2510 fn testnet_spot_state_without_maintenance_map_parses() {
2511 let state: SpotStateResponse =
2512 serde_json::from_str(TESTNET_SHAPE).expect("testnet shape must deserialize");
2513 let balances = map_spot_state(state, None).expect("mapping must succeed");
2514 assert_eq!(balances.len(), 1);
2515 let usdc = &balances[0];
2516 assert_eq!(usdc.token, "USDC");
2517 assert_eq!(usdc.equity.to_string(), "969.0");
2518 assert_eq!(usdc.hold.to_string(), "0.0");
2519 assert_eq!(usdc.free.to_string(), "969.0");
2520 assert_eq!(usdc.usable.to_string(), "969.0");
2522 assert_eq!(usdc.safe, None);
2523 assert_eq!(usdc.maintenance, None);
2524 }
2525
2526 #[test]
2527 fn mainnet_spot_state_with_maintenance_map_parses() {
2528 let state: SpotStateResponse =
2529 serde_json::from_str(MAINNET_SHAPE).expect("mainnet shape must deserialize");
2530 let balances = map_spot_state(state, None).expect("mapping must succeed");
2531 let usdc = &balances[0];
2532 assert_eq!(usdc.equity.to_string(), "969.0");
2533 assert_eq!(usdc.free.to_string(), "959.0");
2534 assert_eq!(usdc.usable.to_string(), "955.0");
2536 assert_eq!(usdc.safe.map(|d| d.to_string()), Some("955.0".to_string()));
2537 assert_eq!(
2538 usdc.maintenance.map(|d| d.to_string()),
2539 Some("14.0".to_string())
2540 );
2541 }
2542
2543 #[test]
2544 fn usdc_balance_carries_perp_margin_used() {
2545 let state: SpotStateResponse =
2546 serde_json::from_str(TESTNET_SHAPE).expect("testnet shape must deserialize");
2547 let balances = map_spot_state(state, Some(Decimal::from(2))).expect("mapping must succeed");
2548 let usdc = &balances[0];
2549 assert_eq!(
2550 usdc.margin_used.map(|d| d.to_string()),
2551 Some("2".to_string())
2552 );
2553 }
2554
2555 #[test]
2559 fn balance_rows_end_with_marked_perp_row() {
2560 let state: SpotStateResponse =
2561 serde_json::from_str(TESTNET_SHAPE).expect("testnet shape must deserialize");
2562 let perp: ClearinghouseStateResponse = serde_json::from_str(
2563 r#"{"marginSummary":{"accountValue":"30.0","totalMarginUsed":"5.0"},"assetPositions":[]}"#,
2564 )
2565 .expect("perp shape must deserialize");
2566 let mut rows =
2567 map_spot_state(state, Some(Decimal::from(2))).expect("spot rows must map");
2568 rows.push(map_perp_state(perp).expect("perp row must map"));
2569
2570 assert_eq!(rows.len(), 2);
2571 assert_eq!(rows[0].token, "USDC");
2572 assert_eq!(rows[0].margin_used.map(|d| d.to_string()), Some("2".to_string()));
2573 assert_eq!(rows[1].token, crate::PERP_LEDGER_TOKEN);
2574 assert_eq!(rows[1].equity.to_string(), "30.0");
2575 assert_eq!(rows[1].free.to_string(), "25.0");
2576 assert_eq!(rows[1].usable.to_string(), "25.0");
2577 assert_eq!(
2578 rows[1].margin_used.map(|d| d.to_string()),
2579 Some("5.0".to_string())
2580 );
2581 assert_eq!(rows[1].settled_usd, None);
2583 assert!(rows[..rows.len() - 1]
2585 .iter()
2586 .all(|b| b.token != crate::PERP_LEDGER_TOKEN));
2587 }
2588
2589 #[test]
2593 fn perp_margin_account_maps_to_usdc_row() {
2594 let state: ClearinghouseStateResponse = serde_json::from_str(
2595 r#"{"marginSummary":{"accountValue":"30.0","totalNtlPos":"0.0","totalRawUsd":"30.0","totalMarginUsed":"0.0"},"assetPositions":[]}"#,
2596 )
2597 .expect("perp shape must deserialize");
2598 let usdc = map_perp_state(state).expect("perp mapping must succeed");
2599 assert_eq!(usdc.token, crate::PERP_LEDGER_TOKEN);
2602 assert_eq!(usdc.settled_usd.map(|d| d.to_string()), Some("30.0".into()));
2604 assert_eq!(usdc.equity.to_string(), "30.0"); assert_eq!(usdc.free.to_string(), "30.0"); assert_eq!(usdc.usable.to_string(), "30.0");
2607 assert_eq!(
2608 usdc.margin_used.map(|d| d.to_string()),
2609 Some("0.0".to_string())
2610 );
2611 }
2612}