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