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