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