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