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