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