1use crate::rate_limiter::{AddressRateLimiter, RestRateLimiter};
2use futures_util::{stream, StreamExt};
3use guilder_abstraction::{
4 self, AssetContext, BoxStream, Deposit, Fill, FundingPayment, L2Update, Liquidation, OpenOrder,
5 OrderPlacement, OrderSide, OrderStatus, OrderType, OrderUpdate, Position, PredictedFunding,
6 Side, TimeInForce, UserFill, Withdrawal,
7};
8use reqwest::Client;
9use rust_decimal::Decimal;
10use serde::Deserialize;
11use serde_json::Value;
12use std::collections::HashMap;
13use std::str::FromStr;
14use std::sync::Arc;
15const HYPERLIQUID_INFO_URL: &str = "https://api.hyperliquid.xyz/info";
16const HYPERLIQUID_EXCHANGE_URL: &str = "https://api.hyperliquid.xyz/exchange";
17
18async fn parse_response<T: for<'de> serde::Deserialize<'de>>(
19 resp: reqwest::Response,
20) -> Result<T, String> {
21 let status = resp.status();
22 let text = resp.text().await.map_err(|e| {
23 format!("failed to read response body (status {status}): {e}")
24 })?;
25
26 if text.is_empty() {
27 return Err(format!(
28 "empty response body from Hyperliquid (HTTP {status})"
29 ));
30 }
31
32 serde_json::from_str(&text).map_err(|e| {
33 let snippet = if text.len() > 512 {
34 format!("{}... ({} bytes total)", &text[..256], text.len())
35 } else {
36 text.clone()
37 };
38 format!("deserialize error (HTTP {status}): {e}: {snippet}")
39 })
40}
41
42pub struct HyperliquidClient {
43 client: Client,
44 user_address: Option<String>,
45 private_key: Option<String>,
46 rest_limiter: Arc<RestRateLimiter>,
47 address_limiter: Arc<AddressRateLimiter>,
48 ws_mux: crate::ws::WsMux,
49}
50
51impl Default for HyperliquidClient {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl HyperliquidClient {
58 pub fn new() -> Self {
59 HyperliquidClient {
60 client: Client::new(),
61 user_address: None,
62 private_key: None,
63 rest_limiter: Arc::new(RestRateLimiter::new()),
64 address_limiter: Arc::new(AddressRateLimiter::new()),
65 ws_mux: crate::ws::WsMux::new(),
66 }
67 }
68
69 pub fn with_auth(user_address: impl Into<String>, private_key: String) -> Self {
70 HyperliquidClient {
71 client: Client::new(),
72 user_address: Some(user_address.into()),
73 private_key: Some(private_key),
74 rest_limiter: Arc::new(RestRateLimiter::new()),
75 address_limiter: Arc::new(AddressRateLimiter::new()),
76 ws_mux: crate::ws::WsMux::new(),
77 }
78 }
79
80 pub fn with_budgets(mut self, rest_weight: u32, addr_budget: u64) -> Self {
83 self.rest_limiter = Arc::new(RestRateLimiter::new_with_budget(rest_weight));
84 self.address_limiter = Arc::new(AddressRateLimiter::new_with_budget(addr_budget));
85 self
86 }
87
88 async fn info_post(
92 &self,
93 body: Value,
94 weight: u32,
95 call: &str,
96 ) -> Result<reqwest::Response, String> {
97 self.rest_limiter.acquire(weight).await.map_err(|e| {
98 format!(
99 "rate_limited: info_post ({call}) budget exhausted, retry_after_ms={}",
100 e.retry_after.as_millis()
101 )
102 })?;
103 self.client
104 .post(HYPERLIQUID_INFO_URL)
105 .json(&body)
106 .send()
107 .await
108 .map_err(|e| e.to_string())
109 }
110 fn require_user_address(&self) -> Result<String, String> {
111 self.user_address
112 .clone()
113 .ok_or_else(|| "user address required: use HyperliquidClient::with_auth".to_string())
114 }
115
116 fn require_private_key(&self) -> Result<&str, String> {
117 self.private_key
118 .as_deref()
119 .ok_or_else(|| "private key required: use HyperliquidClient::with_auth".to_string())
120 }
121
122 async fn get_asset_index(&self, symbol: &str) -> Result<usize, String> {
123 let resp = self
125 .info_post(serde_json::json!({"type": "meta"}), 20, "get_asset_index")
126 .await?;
127 let meta: MetaResponse = parse_response(resp).await?;
128 meta.universe
129 .iter()
130 .position(|a| a.name == symbol)
131 .ok_or_else(|| format!("symbol {} not found", symbol))
132 }
133
134 async fn submit_signed_action(
135 &self,
136 action: Value,
137 vault_address: Option<&str>,
138 ) -> Result<Value, String> {
139 let private_key = self.require_private_key()?;
140 let nonce = std::time::SystemTime::now()
141 .duration_since(std::time::UNIX_EPOCH)
142 .unwrap()
143 .as_millis() as u64;
144
145 let (r, s, v) = sign_action(private_key, &action, vault_address, nonce)?;
146
147 let payload = serde_json::json!({
148 "action": action,
149 "nonce": nonce,
150 "signature": {"r": r, "s": s, "v": v},
151 "vaultAddress": null,
152 "expiresAfter": null
153 });
154
155 self.rest_limiter.acquire(1).await.map_err(|e| {
157 format!(
158 "rate_limited: rest_weight exhausted, retry_after_ms={}",
159 e.retry_after.as_millis()
160 )
161 })?;
162 self.address_limiter.acquire(1, false).await.map_err(|e| {
163 format!(
164 "rate_limited: address quota exhausted, retry_after_ms={}",
165 e.retry_after.as_millis()
166 )
167 })?;
168
169 let resp = self
170 .client
171 .post(HYPERLIQUID_EXCHANGE_URL)
172 .json(&payload)
173 .send()
174 .await
175 .map_err(|e| e.to_string())?;
176
177 let status = resp.status();
178 if !status.is_success() {
179 let text = resp.text().await.map_err(|e| e.to_string())?;
180 return Err(format!("HTTP {status}: {text}"));
181 }
182
183 let body: Value = parse_response(resp).await?;
184 if body["status"].as_str() == Some("err") {
185 return Err(body["response"]
186 .as_str()
187 .unwrap_or("unknown error")
188 .to_string());
189 }
190 Ok(body)
191 }
192}
193
194#[derive(Deserialize)]
197struct MetaResponse {
198 universe: Vec<AssetInfo>,
199}
200
201#[derive(Deserialize)]
202struct AssetInfo {
203 name: String,
204 #[serde(rename = "szDecimals")]
205 sz_decimals: i32,
206}
207
208type MetaAndAssetCtxsResponse = (MetaResponse, Vec<RestAssetCtx>);
209
210#[derive(Deserialize)]
211#[serde(rename_all = "camelCase")]
212#[allow(dead_code)]
213struct RestAssetCtx {
214 open_interest: String,
215 funding: String,
216 mark_px: String,
217 day_ntl_vlm: String,
218 mid_px: Option<String>,
219 oracle_px: Option<String>,
220 premium: Option<String>,
221 prev_day_px: Option<String>,
222}
223
224#[derive(Deserialize)]
225#[serde(rename_all = "camelCase")]
226struct ClearinghouseStateResponse {
227 margin_summary: MarginSummary,
228 asset_positions: Vec<AssetPosition>,
229}
230
231#[derive(Deserialize)]
232#[serde(rename_all = "camelCase")]
233struct MarginSummary {
234 account_value: String,
235}
236
237#[derive(Deserialize)]
238struct AssetPosition {
239 position: PositionDetail,
240}
241
242#[derive(Deserialize)]
243#[serde(rename_all = "camelCase")]
244struct PositionDetail {
245 coin: String,
246 szi: String,
248 entry_px: Option<String>,
249}
250
251#[derive(Deserialize)]
252#[serde(rename_all = "camelCase")]
253struct RestOpenOrder {
254 coin: String,
255 side: String,
256 limit_px: String,
257 sz: String,
258 oid: i64,
259 orig_sz: String,
260 cloid: Option<String>,
261}
262
263type PredictedFundingsResponse = Vec<(String, Vec<(String, Option<PredictedFundingEntry>)>)>;
266
267#[derive(Deserialize)]
268#[serde(rename_all = "camelCase")]
269struct PredictedFundingEntry {
270 funding_rate: String,
271 next_funding_time: i64,
272}
273
274#[derive(Deserialize)]
277struct WsEnvelope {
278 channel: String,
279 #[serde(default)]
280 data: Value,
281}
282
283#[derive(Deserialize)]
284struct WsBook {
285 coin: String,
286 levels: Vec<Vec<WsLevel>>,
287 time: i64,
288}
289
290#[derive(Deserialize)]
291struct WsLevel {
292 px: String,
293 sz: String,
294}
295
296#[derive(Deserialize)]
297#[serde(rename_all = "camelCase")]
298struct WsAssetCtx {
299 coin: String,
300 ctx: WsPerpsCtx,
301}
302
303#[derive(Deserialize)]
304#[serde(rename_all = "camelCase")]
305struct WsPerpsCtx {
306 open_interest: String,
307 funding: String,
308 mark_px: String,
309 day_ntl_vlm: String,
310 mid_px: Option<String>,
311 oracle_px: Option<String>,
312 premium: Option<String>,
313 prev_day_px: Option<String>,
314}
315
316#[derive(Deserialize)]
317struct WsUserEvent {
318 liquidation: Option<WsLiquidation>,
319 fills: Option<Vec<WsUserFill>>,
320 funding: Option<WsFunding>,
321 spot_state: Option<WsSpotState>,
322}
323
324#[derive(Deserialize)]
325struct WsSpotState {
326 balances: Option<Vec<WsSpotBalance>>,
327}
328
329#[derive(Deserialize)]
330struct WsSpotBalance {
331 coin: String,
332 total: String,
333 hold: String,
334}
335
336#[derive(Deserialize)]
337struct WsLiquidation {
338 liquidated_user: String,
339 liquidated_ntl_pos: String,
340 liquidated_account_value: String,
341}
342
343#[derive(Deserialize)]
344struct WsUserFill {
345 coin: String,
346 px: String,
347 sz: String,
348 side: String,
349 time: i64,
350 oid: i64,
351 fee: String,
352 #[serde(default)]
354 cloid: Option<String>,
355}
356
357#[derive(Deserialize)]
358struct WsFunding {
359 time: i64,
360 coin: String,
361 usdc: String,
362}
363
364#[derive(Deserialize)]
365struct WsTrade {
366 coin: String,
367 side: String,
368 px: String,
369 sz: String,
370 time: i64,
371 tid: i64,
372}
373
374#[derive(Deserialize)]
375struct WsOrderUpdate {
376 order: WsOrderInfo,
377 status: String,
378 #[serde(rename = "statusTimestamp")]
379 status_timestamp: i64,
380}
381
382#[derive(Deserialize)]
383#[serde(rename_all = "camelCase")]
384struct WsOrderInfo {
385 coin: String,
386 side: String,
387 limit_px: String,
388 sz: String,
389 oid: i64,
390 orig_sz: String,
391 #[serde(default)]
393 cloid: Option<String>,
394}
395
396#[derive(Deserialize)]
399struct WsLedgerUpdates {
400 updates: Vec<WsLedgerEntry>,
401}
402
403#[derive(Deserialize)]
404struct WsLedgerEntry {
405 time: i64,
406 delta: WsLedgerDelta,
407}
408
409#[derive(Deserialize)]
410struct WsLedgerDelta {
411 #[serde(rename = "type")]
412 kind: String,
413 usdc: Option<String>,
414}
415
416fn parse_decimal(s: &str) -> Option<Decimal> {
419 Decimal::from_str(s).ok()
420}
421
422fn keccak256(data: &[u8]) -> [u8; 32] {
423 use sha3::{Digest, Keccak256};
424 Keccak256::digest(data).into()
425}
426
427fn hyperliquid_domain_separator() -> [u8; 32] {
429 let type_hash = keccak256(
430 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
431 );
432 let name_hash = keccak256(b"Exchange");
433 let version_hash = keccak256(b"1");
434 let mut chain_id = [0u8; 32];
435 chain_id[28..32].copy_from_slice(&1337u32.to_be_bytes());
436 let verifying_contract = [0u8; 32];
437
438 let mut data = [0u8; 160];
439 data[..32].copy_from_slice(&type_hash);
440 data[32..64].copy_from_slice(&name_hash);
441 data[64..96].copy_from_slice(&version_hash);
442 data[96..128].copy_from_slice(&chain_id);
443 data[128..160].copy_from_slice(&verifying_contract);
444 keccak256(&data)
445}
446
447fn value_to_msgpack(val: &Value) -> Vec<u8> {
450 match val {
451 Value::Null => vec![0xc0],
452 Value::Bool(true) => vec![0xc3],
453 Value::Bool(false) => vec![0xc2],
454 Value::Number(n) => {
455 if let Some(i) = n.as_i64() {
456 if i >= 0 {
457 if i <= 127 {
458 vec![i as u8]
459 } else if i <= 255 {
460 vec![0xcc, i as u8]
461 } else if i <= 65535 {
462 let mut buf = vec![0xcd];
463 buf.extend_from_slice(&(i as u16).to_be_bytes());
464 buf
465 } else if i <= 4294967295 {
466 let mut buf = vec![0xce];
467 buf.extend_from_slice(&(i as u32).to_be_bytes());
468 buf
469 } else {
470 let mut buf = vec![0xcf];
471 buf.extend_from_slice(&(i as u64).to_be_bytes());
472 buf
473 }
474 } else if i >= -32 {
475 vec![0xe0 | (i as u8)]
476 } else if i >= -128 {
477 vec![0xd0, i as i8 as u8]
478 } else if i >= -32768 {
479 let mut buf = vec![0xd1];
480 buf.extend_from_slice(&(i as i16).to_be_bytes());
481 buf
482 } else if i >= -2147483648 {
483 let mut buf = vec![0xd2];
484 buf.extend_from_slice(&(i as i32).to_be_bytes());
485 buf
486 } else {
487 let mut buf = vec![0xd3];
488 buf.extend_from_slice(&i.to_be_bytes());
489 buf
490 }
491 } else if let Some(f) = n.as_f64() {
492 let mut buf = vec![0xcb];
493 buf.extend_from_slice(&f.to_be_bytes());
494 buf
495 } else {
496 let u = n.as_u64().unwrap();
497 if u <= 127 {
498 vec![u as u8]
499 } else if u <= 255 {
500 vec![0xcc, u as u8]
501 } else if u <= 65535 {
502 let mut buf = vec![0xcd];
503 buf.extend_from_slice(&(u as u16).to_be_bytes());
504 buf
505 } else if u <= 4294967295 {
506 let mut buf = vec![0xce];
507 buf.extend_from_slice(&(u as u32).to_be_bytes());
508 buf
509 } else {
510 let mut buf = vec![0xcf];
511 buf.extend_from_slice(&u.to_be_bytes());
512 buf
513 }
514 }
515 }
516 Value::String(s) => {
517 let bytes = s.as_bytes();
518 let len = bytes.len();
519 let mut buf = Vec::new();
520 if len <= 31 {
521 buf.push(0xa0 | (len as u8));
522 } else if len <= 255 {
523 buf.push(0xd9);
524 buf.push(len as u8);
525 } else if len <= 65535 {
526 buf.push(0xda);
527 buf.extend_from_slice(&(len as u16).to_be_bytes());
528 } else {
529 buf.push(0xdb);
530 buf.extend_from_slice(&(len as u32).to_be_bytes());
531 }
532 buf.extend_from_slice(bytes);
533 buf
534 }
535 Value::Array(arr) => {
536 let len = arr.len();
537 let mut buf = Vec::new();
538 if len <= 15 {
539 buf.push(0x90 | (len as u8));
540 } else if len <= 65535 {
541 buf.push(0xdc);
542 buf.extend_from_slice(&(len as u16).to_be_bytes());
543 } else {
544 buf.push(0xdd);
545 buf.extend_from_slice(&(len as u32).to_be_bytes());
546 }
547 for item in arr {
548 buf.extend_from_slice(&value_to_msgpack(item));
549 }
550 buf
551 }
552 Value::Object(map) => {
553 let len = map.len();
554 let mut buf = Vec::new();
555 if len <= 15 {
556 buf.push(0x80 | (len as u8));
557 } else if len <= 65535 {
558 buf.push(0xde);
559 buf.extend_from_slice(&(len as u16).to_be_bytes());
560 } else {
561 buf.push(0xdf);
562 buf.extend_from_slice(&(len as u32).to_be_bytes());
563 }
564 for (key, value) in map {
565 buf.extend_from_slice(&value_to_msgpack(&Value::String(key.clone())));
566 buf.extend_from_slice(&value_to_msgpack(value));
567 }
568 buf
569 }
570 }
571}
572
573fn action_to_canonical_msgpack(action: &Value) -> Result<Vec<u8>, String> {
576 Ok(value_to_msgpack(action))
577}
578
579fn build_order_msgpack(
582 asset_idx: usize,
583 is_buy: bool,
584 price: &str,
585 size: &str,
586 reduce_only: bool,
587 order_kind: &str,
588 tif: &[u8],
589 cloid: Option<&str>,
590) -> Vec<u8> {
591 let field_count = if cloid.is_some() { 7 } else { 6 };
592 let mut buf = Vec::new();
593 buf.push(0x80 | (field_count as u8)); buf.extend_from_slice(&value_to_msgpack(&Value::String("a".to_string())));
597 buf.extend_from_slice(&value_to_msgpack(&Value::Number(serde_json::Number::from(asset_idx))));
598
599 buf.extend_from_slice(&value_to_msgpack(&Value::String("b".to_string())));
601 buf.push(if is_buy { 0xc3 } else { 0xc2 });
602
603 buf.extend_from_slice(&value_to_msgpack(&Value::String("p".to_string())));
605 buf.extend_from_slice(&value_to_msgpack(&Value::String(price.to_string())));
606
607 buf.extend_from_slice(&value_to_msgpack(&Value::String("s".to_string())));
609 buf.extend_from_slice(&value_to_msgpack(&Value::String(size.to_string())));
610
611 buf.extend_from_slice(&value_to_msgpack(&Value::String("r".to_string())));
613 buf.push(if reduce_only { 0xc3 } else { 0xc2 });
614
615 buf.extend_from_slice(&value_to_msgpack(&Value::String("t".to_string())));
617 buf.push(0x81);
619 buf.extend_from_slice(&value_to_msgpack(&Value::String(order_kind.to_string())));
620 buf.push(0x81);
622 buf.extend_from_slice(&value_to_msgpack(&Value::String("tif".to_string())));
623 buf.extend_from_slice(&value_to_msgpack(&Value::String(String::from_utf8_lossy(tif).to_string())));
624
625 if let Some(c) = cloid {
627 buf.extend_from_slice(&value_to_msgpack(&Value::String("c".to_string())));
628 buf.extend_from_slice(&value_to_msgpack(&Value::String(c.to_string())));
629 }
630
631 buf
632}
633
634fn build_trigger_order_msgpack(
637 asset_idx: usize,
638 is_buy: bool,
639 price: &str,
640 size: &str,
641 reduce_only: bool,
642 trigger_px: &str,
643 is_market: bool,
644 tpsl: &str,
645 cloid: Option<&str>,
646) -> Vec<u8> {
647 let field_count = if cloid.is_some() { 7 } else { 6 };
648 let mut buf = Vec::new();
649 buf.push(0x80 | (field_count as u8)); buf.extend_from_slice(&value_to_msgpack(&Value::String("a".to_string())));
653 buf.extend_from_slice(&value_to_msgpack(&Value::Number(serde_json::Number::from(asset_idx))));
654
655 buf.extend_from_slice(&value_to_msgpack(&Value::String("b".to_string())));
657 buf.push(if is_buy { 0xc3 } else { 0xc2 });
658
659 buf.extend_from_slice(&value_to_msgpack(&Value::String("p".to_string())));
661 buf.extend_from_slice(&value_to_msgpack(&Value::String(price.to_string())));
662
663 buf.extend_from_slice(&value_to_msgpack(&Value::String("s".to_string())));
665 buf.extend_from_slice(&value_to_msgpack(&Value::String(size.to_string())));
666
667 buf.extend_from_slice(&value_to_msgpack(&Value::String("r".to_string())));
669 buf.push(if reduce_only { 0xc3 } else { 0xc2 });
670
671 buf.extend_from_slice(&value_to_msgpack(&Value::String("t".to_string())));
673 buf.push(0x81); buf.extend_from_slice(&value_to_msgpack(&Value::String("trigger".to_string())));
675 buf.push(0x83);
677 buf.extend_from_slice(&value_to_msgpack(&Value::String("isMarket".to_string())));
678 buf.push(if is_market { 0xc3 } else { 0xc2 });
679 buf.extend_from_slice(&value_to_msgpack(&Value::String("triggerPx".to_string())));
680 buf.extend_from_slice(&value_to_msgpack(&Value::String(trigger_px.to_string())));
681 buf.extend_from_slice(&value_to_msgpack(&Value::String("tpsl".to_string())));
682 buf.extend_from_slice(&value_to_msgpack(&Value::String(tpsl.to_string())));
683
684 if let Some(c) = cloid {
686 buf.extend_from_slice(&value_to_msgpack(&Value::String("c".to_string())));
687 buf.extend_from_slice(&value_to_msgpack(&Value::String(c.to_string())));
688 }
689
690 buf
691}
692
693fn sign_with_msgpack(
695 msgpack: &[u8],
696 private_key: &str,
697 nonce: u64,
698 vault_address: Option<&str>,
699) -> Result<(String, String, u8), String> {
700 use k256::ecdsa::SigningKey;
701
702 let mut data = msgpack.to_vec();
703 data.extend_from_slice(&nonce.to_be_bytes());
704 match vault_address {
705 None => data.push(0u8),
706 Some(addr) => {
707 data.push(1u8);
708 let addr_bytes = hex::decode(addr.trim_start_matches("0x"))
709 .map_err(|e| format!("invalid vault address: {}", e))?;
710 data.extend_from_slice(&addr_bytes);
711 }
712 }
713
714 let connection_id = keccak256(&data);
715 let agent_type_hash = keccak256(b"Agent(string source,bytes32 connectionId)");
716 let source_hash = keccak256(b"a");
717 let mut struct_data = [0u8; 96];
718 struct_data[..32].copy_from_slice(&agent_type_hash);
719 struct_data[32..64].copy_from_slice(&source_hash);
720 struct_data[64..96].copy_from_slice(&connection_id);
721 let struct_hash = keccak256(&struct_data);
722
723 let domain_sep = hyperliquid_domain_separator();
724 let mut final_data = Vec::with_capacity(66);
725 final_data.extend_from_slice(b"\x19\x01");
726 final_data.extend_from_slice(&domain_sep);
727 final_data.extend_from_slice(&struct_hash);
728 let final_hash = keccak256(&final_data);
729
730 let key_bytes = hex::decode(private_key.trim_start_matches("0x"))
731 .map_err(|e| format!("invalid private key: {}", e))?;
732 let signing_key =
733 SigningKey::from_bytes(key_bytes.as_slice().into()).map_err(|e| e.to_string())?;
734 let (sig, recovery_id) = signing_key
735 .sign_prehash_recoverable(&final_hash)
736 .map_err(|e| e.to_string())?;
737
738 let sig_bytes = sig.to_bytes();
739 let r = format!("0x{}", hex::encode(&sig_bytes[..32]));
740 let s = format!("0x{}", hex::encode(&sig_bytes[32..64]));
741 let v = 27u8 + recovery_id.to_byte();
742
743 Ok((r, s, v))
744}
745
746fn sign_action(
749 private_key: &str,
750 action: &Value,
751 vault_address: Option<&str>,
752 nonce: u64,
753) -> Result<(String, String, u8), String> {
754 use k256::ecdsa::SigningKey;
755
756 let msgpack_bytes = action_to_canonical_msgpack(action)?;
759 let mut data = msgpack_bytes;
760 data.extend_from_slice(&nonce.to_be_bytes());
761 match vault_address {
762 None => data.push(0u8),
763 Some(addr) => {
764 data.push(1u8);
765 let addr_bytes = hex::decode(addr.trim_start_matches("0x"))
766 .map_err(|e| format!("invalid vault address: {}", e))?;
767 data.extend_from_slice(&addr_bytes);
768 }
769 }
770 let connection_id = keccak256(&data);
771
772 let agent_type_hash = keccak256(b"Agent(string source,bytes32 connectionId)");
774 let source_hash = keccak256(b"a"); let mut struct_data = [0u8; 96];
776 struct_data[..32].copy_from_slice(&agent_type_hash);
777 struct_data[32..64].copy_from_slice(&source_hash);
778 struct_data[64..96].copy_from_slice(&connection_id);
779 let struct_hash = keccak256(&struct_data);
780
781 let domain_sep = hyperliquid_domain_separator();
783 let mut final_data = Vec::with_capacity(66);
784 final_data.extend_from_slice(b"\x19\x01");
785 final_data.extend_from_slice(&domain_sep);
786 final_data.extend_from_slice(&struct_hash);
787 let final_hash = keccak256(&final_data);
788
789 let key_bytes = hex::decode(private_key.trim_start_matches("0x"))
791 .map_err(|e| format!("invalid private key: {}", e))?;
792 let signing_key =
793 SigningKey::from_bytes(key_bytes.as_slice().into()).map_err(|e| e.to_string())?;
794 let (sig, recovery_id) = signing_key
795 .sign_prehash_recoverable(&final_hash)
796 .map_err(|e| e.to_string())?;
797
798 let sig_bytes = sig.to_bytes();
799 let r = format!("0x{}", hex::encode(&sig_bytes[..32]));
800 let s = format!("0x{}", hex::encode(&sig_bytes[32..64]));
801 let v = 27u8 + recovery_id.to_byte();
802
803 Ok((r, s, v))
804}
805
806#[allow(async_fn_in_trait)]
809impl guilder_abstraction::TestServer for HyperliquidClient {
810 async fn ping(&self) -> Result<bool, String> {
812 self.info_post(serde_json::json!({"type": "allMids"}), 2, "ping")
814 .await
815 .map(|r| r.status().is_success())
816 }
817
818 async fn get_server_time(&self) -> Result<i64, String> {
820 Ok(std::time::SystemTime::now()
821 .duration_since(std::time::UNIX_EPOCH)
822 .map(|d| d.as_millis() as i64)
823 .unwrap_or(0))
824 }
825}
826
827#[allow(async_fn_in_trait)]
828impl guilder_abstraction::GetMarketData for HyperliquidClient {
829 async fn get_symbol(&self) -> Result<Vec<String>, String> {
831 let resp = self
833 .info_post(serde_json::json!({"type": "meta"}), 20, "get_symbol")
834 .await?;
835 parse_response::<MetaResponse>(resp)
836 .await
837 .map(|r| r.universe.into_iter().map(|a| a.name).collect())
838 }
839
840 async fn get_open_interest(&self, symbol: String) -> Result<Decimal, String> {
842 let resp = self
844 .info_post(
845 serde_json::json!({"type": "metaAndAssetCtxs"}),
846 20,
847 "get_open_interest",
848 )
849 .await?;
850 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
851 .await?
852 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
853 meta.universe
854 .iter()
855 .position(|a| a.name == symbol)
856 .and_then(|i| ctxs.get(i))
857 .and_then(|ctx| parse_decimal(&ctx.open_interest))
858 .ok_or_else(|| format!("symbol {} not found", symbol))
859 }
860
861 async fn get_asset_context(&self, symbol: String) -> Result<AssetContext, String> {
863 let resp = self
865 .info_post(
866 serde_json::json!({"type": "metaAndAssetCtxs"}),
867 20,
868 "get_asset_context",
869 )
870 .await?;
871 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
872 .await?
873 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
874 let idx = meta
875 .universe
876 .iter()
877 .position(|a| a.name == symbol)
878 .ok_or_else(|| format!("symbol {} not found", symbol))?;
879 let ctx = ctxs
880 .get(idx)
881 .ok_or_else(|| format!("symbol {} not found", symbol))?;
882 Ok(AssetContext {
883 symbol,
884 open_interest: parse_decimal(&ctx.open_interest).ok_or("invalid open_interest")?,
885 funding_rate: parse_decimal(&ctx.funding).ok_or("invalid funding")?,
886 mark_price: parse_decimal(&ctx.mark_px).ok_or("invalid mark_px")?,
887 day_volume: parse_decimal(&ctx.day_ntl_vlm).ok_or("invalid day_ntl_vlm")?,
888 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
889 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
890 premium: ctx.premium.as_deref().and_then(parse_decimal),
891 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
892 sz_decimals: meta.universe.get(idx).map(|a| a.sz_decimals).unwrap_or(0),
893 })
894 }
895
896 async fn get_all_asset_contexts(&self) -> Result<Vec<AssetContext>, String> {
899 let resp = self
901 .info_post(
902 serde_json::json!({"type": "metaAndAssetCtxs"}),
903 20,
904 "get_all_asset_contexts",
905 )
906 .await?;
907 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
908 .await?
909 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
910 let mut result = Vec::with_capacity(meta.universe.len());
911 for (asset, ctx) in meta.universe.iter().zip(ctxs.iter()) {
912 let Some(open_interest) = parse_decimal(&ctx.open_interest) else {
913 continue;
914 };
915 let Some(funding_rate) = parse_decimal(&ctx.funding) else {
916 continue;
917 };
918 let Some(mark_price) = parse_decimal(&ctx.mark_px) else {
919 continue;
920 };
921 let Some(day_volume) = parse_decimal(&ctx.day_ntl_vlm) else {
922 continue;
923 };
924 result.push(AssetContext {
925 symbol: asset.name.clone(),
926 open_interest,
927 funding_rate,
928 mark_price,
929 day_volume,
930 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
931 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
932 premium: ctx.premium.as_deref().and_then(parse_decimal),
933 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
934 sz_decimals: asset.sz_decimals,
935 });
936 }
937 Ok(result)
938 }
939
940 async fn get_sz_decimals(&self, symbol: String) -> Result<i32, String> {
942 let all = self.get_all_sz_decimals().await?;
943 all.get(&symbol)
944 .copied()
945 .ok_or_else(|| format!("symbol {} not found", symbol))
946 }
947
948 async fn get_all_sz_decimals(&self) -> Result<HashMap<String, i32>, String> {
950 let resp = self
952 .info_post(
953 serde_json::json!({"type": "metaAndAssetCtxs"}),
954 20,
955 "get_all_sz_decimals",
956 )
957 .await?;
958 let (meta, _) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
959 .await?
960 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
961 Ok(meta
962 .universe
963 .into_iter()
964 .map(|a| (a.name, a.sz_decimals))
965 .collect())
966 }
967
968 async fn get_l2_orderbook(&self, symbol: String) -> Result<Vec<L2Update>, String> {
971 let resp = self
973 .info_post(
974 serde_json::json!({"type": "l2Book", "coin": symbol}),
975 2,
976 "get_l2_orderbook",
977 )
978 .await?;
979 let book: Option<WsBook> = parse_response(resp).await?;
980 let book = match book {
981 Some(b) => b,
982 None => return Ok(vec![]),
983 };
984 let mut levels = Vec::new();
985 for level in book.levels.first().into_iter().flatten() {
986 if let (Some(price), Some(volume)) =
987 (parse_decimal(&level.px), parse_decimal(&level.sz))
988 {
989 levels.push(L2Update {
990 symbol: book.coin.clone(),
991 price,
992 volume,
993 side: Side::Ask,
994 sequence: book.time,
995 });
996 }
997 }
998 for level in book.levels.get(1).into_iter().flatten() {
999 if let (Some(price), Some(volume)) =
1000 (parse_decimal(&level.px), parse_decimal(&level.sz))
1001 {
1002 levels.push(L2Update {
1003 symbol: book.coin.clone(),
1004 price,
1005 volume,
1006 side: Side::Bid,
1007 sequence: book.time,
1008 });
1009 }
1010 }
1011 Ok(levels)
1012 }
1013
1014 async fn get_price(&self, symbol: String) -> Result<Decimal, String> {
1016 let resp = self
1018 .info_post(serde_json::json!({"type": "allMids"}), 2, "get_price")
1019 .await?;
1020 parse_response::<HashMap<String, String>>(resp)
1021 .await?
1022 .get(&symbol)
1023 .and_then(|s| parse_decimal(s))
1024 .ok_or_else(|| format!("symbol {} not found", symbol))
1025 }
1026
1027 async fn get_predicted_fundings(&self) -> Result<Vec<PredictedFunding>, String> {
1030 let resp = self
1032 .info_post(
1033 serde_json::json!({"type": "predictedFundings"}),
1034 20,
1035 "get_predicted_fundings",
1036 )
1037 .await?;
1038 let data: PredictedFundingsResponse = parse_response(resp).await?;
1039 let mut result = Vec::new();
1040 for (symbol, venues) in data {
1041 for (venue, entry) in venues {
1042 let Some(entry) = entry else { continue };
1043 if let Some(funding_rate) = parse_decimal(&entry.funding_rate) {
1044 result.push(PredictedFunding {
1045 symbol: symbol.clone(),
1046 venue,
1047 funding_rate,
1048 next_funding_time_ms: entry.next_funding_time,
1049 });
1050 }
1051 }
1052 }
1053 Ok(result)
1054 }
1055}
1056
1057#[allow(async_fn_in_trait)]
1058impl guilder_abstraction::ManageOrder for HyperliquidClient {
1059 async fn place_order(
1070 &self,
1071 symbol: String,
1072 side: OrderSide,
1073 price: Decimal,
1074 volume: Decimal,
1075 order_type: OrderType,
1076 time_in_force: TimeInForce,
1077 trigger_price: Option<Decimal>,
1078 reduce_only: bool,
1079 cloid: Option<String>,
1080 ) -> Result<OrderPlacement, String> {
1081 let asset_idx = self.get_asset_index(&symbol).await?;
1083 let is_buy = matches!(side, OrderSide::Buy);
1084
1085 let tif_str = match time_in_force {
1086 TimeInForce::Gtc => "Gtc",
1087 TimeInForce::Ioc => "Ioc",
1088 TimeInForce::Fok => "Fok",
1089 TimeInForce::Alo => "Alo",
1090 };
1091
1092 let cloid_hex = cloid.clone();
1093
1094 let is_trigger = matches!(order_type, OrderType::TakeProfit | OrderType::StopLoss);
1096
1097 let (order_msgpack, order_type_json) = if is_trigger {
1098 let trigger_px = trigger_price
1100 .ok_or_else(|| {
1101 format!("{:?} order requires trigger_price to be set", order_type)
1102 })?
1103 .normalize()
1104 .to_string();
1105
1106 let tpsl = match order_type {
1107 OrderType::TakeProfit => "tp",
1108 OrderType::StopLoss => "sl",
1109 _ => unreachable!(),
1110 };
1111
1112 let is_market = matches!(time_in_force, TimeInForce::Ioc);
1115
1116 let price_str = if is_market {
1119 trigger_px.clone()
1120 } else {
1121 price.normalize().to_string()
1122 };
1123
1124 let msgpack = build_trigger_order_msgpack(
1125 asset_idx,
1126 is_buy,
1127 &price_str,
1128 &volume.normalize().to_string(),
1129 reduce_only,
1130 &trigger_px,
1131 is_market,
1132 tpsl,
1133 cloid_hex.as_deref(),
1134 );
1135
1136 let json_type = if is_market {
1137 format!(
1138 r#"{{"trigger":{{"isMarket":true,"triggerPx":"{}","tpsl":"{}"}}}}"#,
1139 trigger_px, tpsl
1140 )
1141 } else {
1142 format!(
1143 r#"{{"trigger":{{"isMarket":false,"triggerPx":"{}","tpsl":"{}"}}}}"#,
1144 trigger_px, tpsl
1145 )
1146 };
1147
1148 (msgpack, json_type)
1149 } else {
1150 let (order_kind, tif_bytes) = match order_type {
1152 OrderType::Limit => ("limit", tif_str.as_bytes()),
1153 OrderType::Market => ("limit", b"Ioc".as_slice()),
1154 _ => unreachable!(),
1155 };
1156
1157 let price_str = price.normalize().to_string();
1158 let size_str = volume.normalize().to_string();
1159
1160 let msgpack = build_order_msgpack(
1161 asset_idx,
1162 is_buy,
1163 &price_str,
1164 &size_str,
1165 reduce_only,
1166 order_kind,
1167 tif_bytes,
1168 cloid_hex.as_deref(),
1169 );
1170
1171 let json_type = match order_type {
1172 OrderType::Limit => format!(r#"{{"limit":{{"tif":"{tif_str}"}}}}"#),
1173 OrderType::Market => r#"{"limit":{"tif":"Ioc"}}"#.to_string(),
1174 _ => unreachable!(),
1175 };
1176
1177 (msgpack, json_type)
1178 };
1179
1180 let mut action_msgpack = Vec::new();
1183 action_msgpack.push(0x83); action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("type".to_string())));
1185 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("order".to_string())));
1186 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("orders".to_string())));
1187 action_msgpack.push(0x91); action_msgpack.extend_from_slice(&order_msgpack);
1189 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("grouping".to_string())));
1190 action_msgpack.extend_from_slice(&value_to_msgpack(&Value::String("na".to_string())));
1191
1192 let cloid_json = if let Some(ref c) = cloid_hex {
1193 format!(r#","c":"{c}""#)
1194 } else {
1195 String::new()
1196 };
1197
1198 let reduce_json = if reduce_only { "true" } else { "false" };
1199
1200 let price_for_json = if is_trigger && matches!(time_in_force, TimeInForce::Ioc) {
1201 if let Some(ref tp) = trigger_price {
1203 tp.normalize().to_string()
1204 } else {
1205 price.normalize().to_string()
1206 }
1207 } else {
1208 price.normalize().to_string()
1209 };
1210
1211 let action_json_str = format!(
1212 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"}}"#,
1213 price = price_for_json,
1214 size = volume.normalize().to_string(),
1215 );
1216
1217 let private_key = self.require_private_key()?;
1219 let nonce = std::time::SystemTime::now()
1220 .duration_since(std::time::UNIX_EPOCH)
1221 .unwrap()
1222 .as_millis() as u64;
1223
1224 let (r, s, v) = sign_with_msgpack(&action_msgpack, private_key, nonce, None)?;
1225
1226 let payload_str = format!(
1227 r#"{{"action":{},"nonce":{},"signature":{{"r":"{}","s":"{}","v":{}}},"vaultAddress":null,"expiresAfter":null}}"#,
1228 action_json_str,
1229 nonce,
1230 r, s, v
1231 );
1232
1233 self.rest_limiter.acquire(1).await.map_err(|e| {
1234 format!(
1235 "rate_limited: rest_weight exhausted, retry_after_ms={}",
1236 e.retry_after.as_millis()
1237 )
1238 })?;
1239 self.address_limiter.acquire(1, false).await.map_err(|e| {
1240 format!(
1241 "rate_limited: address quota exhausted, retry_after_ms={}",
1242 e.retry_after.as_millis()
1243 )
1244 })?;
1245
1246 let resp = self
1247 .client
1248 .post(HYPERLIQUID_EXCHANGE_URL)
1249 .header("Content-Type", "application/json")
1250 .body(payload_str)
1251 .send()
1252 .await
1253 .map_err(|e| e.to_string())?;
1254
1255 let status = resp.status();
1256 if !status.is_success() {
1257 let text = resp.text().await.map_err(|e| e.to_string())?;
1258 return Err(format!("HTTP {status}: {text}"));
1259 }
1260
1261 let body: Value = parse_response(resp).await?;
1262 if body["status"].as_str() == Some("err") {
1263 return Err(body["response"]
1264 .as_str()
1265 .unwrap_or("unknown error")
1266 .to_string());
1267 }
1268 let statuses = &body["response"]["data"]["statuses"][0];
1269
1270 let (oid, returned_cloid, timestamp_ms) = if let Some(resting) = statuses.get("resting") {
1271 let oid = resting["oid"]
1272 .as_i64()
1273 .ok_or_else(|| format!("resting status missing oid: {}", body))?;
1274 let returned_cloid = resting["cloid"].as_str().map(|s: &str| s.to_string());
1275 let ts = std::time::SystemTime::now()
1277 .duration_since(std::time::UNIX_EPOCH)
1278 .unwrap()
1279 .as_millis() as i64;
1280 (oid, returned_cloid, ts)
1281 } else if let Some(filled) = statuses.get("filled") {
1282 let oid = filled["oid"]
1283 .as_i64()
1284 .ok_or_else(|| format!("filled status missing oid: {}", body))?;
1285 let ts = std::time::SystemTime::now()
1287 .duration_since(std::time::UNIX_EPOCH)
1288 .unwrap()
1289 .as_millis() as i64;
1290 (oid, None, ts)
1291 } else if let Some(error) = statuses.get("error") {
1292 return Err(error
1293 .as_str()
1294 .unwrap_or("order rejected with unknown error")
1295 .to_string());
1296 } else {
1297 return Err(format!("unexpected order status: {}", body));
1298 };
1299
1300 Ok(OrderPlacement {
1301 order_id: oid,
1302 symbol,
1303 side,
1304 price,
1305 quantity: volume,
1306 timestamp_ms,
1307 cloid: returned_cloid.or(cloid),
1308 order_type,
1309 trigger_price,
1310 reduce_only,
1311 })
1312 }
1313
1314 async fn change_order_by_cloid(
1317 &self,
1318 cloid: i64,
1319 price: Decimal,
1320 volume: Decimal,
1321 ) -> Result<i64, String> {
1322 let user = self.require_user_address()?;
1323
1324 let resp = self
1326 .info_post(
1327 serde_json::json!({"type": "openOrders", "user": user}),
1328 20,
1329 "change_order_by_cloid",
1330 )
1331 .await?;
1332 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1333 let order = orders
1334 .iter()
1335 .find(|o| o.oid == cloid)
1336 .ok_or_else(|| format!("order {} not found", cloid))?;
1337
1338 let asset_idx = self.get_asset_index(&order.coin).await?;
1339 let is_buy = order.side == "B";
1340
1341 let action = serde_json::json!({
1342 "type": "batchModify",
1343 "modifies": [{
1344 "oid": cloid,
1345 "order": {
1346 "a": asset_idx,
1347 "b": is_buy,
1348 "p": price.to_string(),
1349 "s": volume.to_string(),
1350 "r": false,
1351 "t": {"limit": {"tif": "Gtc"}}
1352 }
1353 }]
1354 });
1355
1356 self.submit_signed_action(action, None).await?;
1357 Ok(cloid)
1358 }
1359
1360 async fn cancel_order_by_cloid(&self, cloid: String) -> Result<(), String> {
1365 let user = self.require_user_address()?;
1366
1367 let resp = self
1369 .info_post(
1370 serde_json::json!({"type": "openOrders", "user": user}),
1371 20,
1372 "cancel_order_by_cloid",
1373 )
1374 .await?;
1375 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1376 let order = orders
1377 .iter()
1378 .find(|o| o.cloid.as_ref() == Some(&cloid))
1379 .ok_or_else(|| format!("order with cloid {} not found", cloid))?;
1380
1381 let meta_resp = self
1383 .info_post(serde_json::json!({"type": "meta"}), 20, "cancel_order_by_cloid")
1384 .await?;
1385 let meta: MetaResponse = parse_response(meta_resp).await?;
1386
1387 let asset_idx = meta
1388 .universe
1389 .iter()
1390 .position(|a| a.name == order.coin)
1391 .ok_or_else(|| format!("asset {} not found in meta", order.coin))?;
1392
1393 let action = serde_json::json!({
1396 "type": "cancel",
1397 "cancels": [{"a": asset_idx, "o": order.oid}]
1398 });
1399
1400 self.submit_signed_action(action, None).await?;
1401 Ok(())
1402 }
1403
1404 async fn cancel_all_order(&self) -> Result<bool, String> {
1407 let user = self.require_user_address()?;
1408
1409 let resp = self
1411 .info_post(
1412 serde_json::json!({"type": "openOrders", "user": user}),
1413 20,
1414 "cancel_all_order",
1415 )
1416 .await?;
1417 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1418 if orders.is_empty() {
1419 return Ok(true);
1420 }
1421
1422 let meta_resp = self
1424 .info_post(serde_json::json!({"type": "meta"}), 20, "cancel_all_order")
1425 .await?;
1426 let meta: MetaResponse = parse_response(meta_resp).await?;
1427
1428 let cancels: Vec<Value> = orders
1429 .iter()
1430 .filter_map(|o| {
1431 let asset_idx = meta.universe.iter().position(|a| a.name == o.coin)?;
1432 Some(serde_json::json!({"a": asset_idx, "o": o.oid}))
1433 })
1434 .collect();
1435
1436 let action = serde_json::json!({"type": "cancel", "cancels": cancels});
1437 self.submit_signed_action(action, None).await?;
1438 Ok(true)
1439 }
1440}
1441
1442#[allow(async_fn_in_trait)]
1443impl guilder_abstraction::SubscribeMarketData for HyperliquidClient {
1444 fn subscribe_l2_update(&self, symbol: String) -> BoxStream<Result<L2Update, String>> {
1445 let sub = serde_json::json!({
1446 "method": "subscribe",
1447 "subscription": {"type": "l2Book", "coin": symbol.clone()}
1448 });
1449 let key = crate::ws::SubKey {
1450 channel: "l2Book".to_string(),
1451 routing_key: symbol,
1452 };
1453 let stream = self.ws_mux.subscribe(key, sub);
1454 Box::pin(async_stream::stream! {
1455 for await msg in stream {
1456 let Ok(env) = serde_json::from_str::<WsEnvelope>(&msg) else {
1457 continue;
1458 };
1459 if env.channel != "l2Book" {
1460 continue;
1461 }
1462 let Ok(book) = serde_json::from_value::<WsBook>(env.data) else {
1463 continue;
1464 };
1465 for level in book.levels.first().into_iter().flatten() {
1466 if let (Some(price), Some(volume)) =
1467 (parse_decimal(&level.px), parse_decimal(&level.sz))
1468 {
1469 yield Ok(L2Update {
1470 symbol: book.coin.clone(),
1471 price,
1472 volume,
1473 side: Side::Ask,
1474 sequence: book.time,
1475 });
1476 }
1477 }
1478 for level in book.levels.get(1).into_iter().flatten() {
1479 if let (Some(price), Some(volume)) =
1480 (parse_decimal(&level.px), parse_decimal(&level.sz))
1481 {
1482 yield Ok(L2Update {
1483 symbol: book.coin.clone(),
1484 price,
1485 volume,
1486 side: Side::Bid,
1487 sequence: book.time,
1488 });
1489 }
1490 }
1491 }
1492 })
1493 }
1494
1495 fn subscribe_asset_context(&self, symbol: String) -> BoxStream<Result<AssetContext, String>> {
1496 let sub = serde_json::json!({
1497 "method": "subscribe",
1498 "subscription": {"type": "activeAssetCtx", "coin": symbol.clone()}
1499 });
1500 let key = crate::ws::SubKey {
1501 channel: "activeAssetCtx".to_string(),
1502 routing_key: symbol,
1503 };
1504 let stream = self.ws_mux.subscribe(key, sub);
1505 Box::pin(async_stream::stream! {
1506 for await msg in stream {
1507 let Ok(env) = serde_json::from_str::<WsEnvelope>(&msg) else {
1508 continue;
1509 };
1510 if env.channel != "activeAssetCtx" {
1511 continue;
1512 }
1513 let Ok(update) = serde_json::from_value::<WsAssetCtx>(env.data) else {
1514 continue;
1515 };
1516 let ctx = &update.ctx;
1517 let (Some(open_interest), Some(funding_rate), Some(mark_price), Some(day_volume)) = (
1518 parse_decimal(&ctx.open_interest),
1519 parse_decimal(&ctx.funding),
1520 parse_decimal(&ctx.mark_px),
1521 parse_decimal(&ctx.day_ntl_vlm),
1522 ) else {
1523 continue;
1524 };
1525 yield Ok(AssetContext {
1526 symbol: update.coin,
1527 open_interest,
1528 funding_rate,
1529 mark_price,
1530 day_volume,
1531 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
1532 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
1533 premium: ctx.premium.as_deref().and_then(parse_decimal),
1534 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
1535 sz_decimals: 0,
1537 });
1538 }
1539 })
1540 }
1541
1542 fn subscribe_liquidation(&self, user: String) -> BoxStream<Result<Liquidation, String>> {
1543 let sub = serde_json::json!({
1544 "method": "subscribe",
1545 "subscription": {"type": "userEvents", "user": user.clone()}
1546 });
1547 let key = crate::ws::SubKey {
1548 channel: "user".to_string(),
1549 routing_key: user,
1550 };
1551 let raw_stream = self.ws_mux.subscribe(key, sub);
1552 Box::pin(
1553 raw_stream
1554 .filter_map(|text| async move {
1555 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1556 return None;
1557 };
1558 if env.channel != "user" {
1559 return None;
1560 }
1561 let Ok(event) = serde_json::from_value::<WsUserEvent>(env.data) else {
1562 return None;
1563 };
1564 let liq = event.liquidation?;
1565 let (Some(notional_position), Some(account_value)) = (
1566 parse_decimal(&liq.liquidated_ntl_pos),
1567 parse_decimal(&liq.liquidated_account_value),
1568 ) else {
1569 return None;
1570 };
1571 let item = Liquidation {
1572 symbol: String::new(),
1573 side: OrderSide::Sell,
1574 liquidated_user: liq.liquidated_user,
1575 notional_position,
1576 account_value,
1577 };
1578 Some(stream::iter(vec![Ok(item)]))
1579 })
1580 .flatten(),
1581 )
1582 }
1583
1584 fn subscribe_fill(&self, symbol: String) -> BoxStream<Result<Fill, String>> {
1585 let sub = serde_json::json!({
1586 "method": "subscribe",
1587 "subscription": {"type": "trades", "coin": symbol.clone()}
1588 });
1589 let key = crate::ws::SubKey {
1590 channel: "trades".to_string(),
1591 routing_key: symbol,
1592 };
1593 let stream = self.ws_mux.subscribe(key, sub);
1594 Box::pin(async_stream::stream! {
1595 for await msg in stream {
1596 let Ok(env) = serde_json::from_str::<WsEnvelope>(&msg) else {
1597 continue;
1598 };
1599 if env.channel != "trades" {
1600 continue;
1601 }
1602 let Ok(trades) = serde_json::from_value::<Vec<WsTrade>>(env.data) else {
1603 continue;
1604 };
1605 for trade in trades {
1606 let side = if trade.side == "B" {
1607 OrderSide::Buy
1608 } else {
1609 OrderSide::Sell
1610 };
1611 let price = parse_decimal(&trade.px);
1612 let volume = parse_decimal(&trade.sz);
1613 if let (Some(price), Some(volume)) = (price, volume) {
1614 yield Ok(Fill {
1615 symbol: trade.coin,
1616 price,
1617 volume,
1618 side,
1619 timestamp_ms: trade.time,
1620 trade_id: trade.tid,
1621 });
1622 }
1623 }
1624 }
1625 })
1626 }
1627}
1628
1629#[allow(async_fn_in_trait)]
1630impl guilder_abstraction::GetAccountSnapshot for HyperliquidClient {
1631 async fn get_positions(&self) -> Result<Vec<Position>, String> {
1634 let user = self.require_user_address()?;
1635 let resp = self
1637 .info_post(
1638 serde_json::json!({"type": "clearinghouseState", "user": user}),
1639 2,
1640 "get_positions",
1641 )
1642 .await?;
1643 let state: ClearinghouseStateResponse = parse_response(resp).await?;
1644
1645 Ok(state
1646 .asset_positions
1647 .into_iter()
1648 .filter_map(|ap| {
1649 let p = ap.position;
1650 let size = parse_decimal(&p.szi)?;
1651 if size.is_zero() {
1652 return None;
1653 }
1654 let entry_price = p
1655 .entry_px
1656 .as_deref()
1657 .and_then(parse_decimal)
1658 .unwrap_or_default();
1659 let side = if size > Decimal::ZERO {
1660 OrderSide::Buy
1661 } else {
1662 OrderSide::Sell
1663 };
1664 Some(Position {
1665 symbol: p.coin,
1666 side,
1667 size: size.abs(),
1668 entry_price,
1669 })
1670 })
1671 .collect())
1672 }
1673
1674 async fn get_open_orders(&self) -> Result<Vec<OpenOrder>, String> {
1677 let user = self.require_user_address()?;
1678 let resp = self
1680 .info_post(
1681 serde_json::json!({"type": "openOrders", "user": user}),
1682 20,
1683 "get_open_orders",
1684 )
1685 .await?;
1686 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1687
1688 Ok(orders
1689 .into_iter()
1690 .filter_map(|o| {
1691 let price = parse_decimal(&o.limit_px)?;
1692 let quantity = parse_decimal(&o.orig_sz)?;
1693 let remaining = parse_decimal(&o.sz)?;
1694 let filled_quantity = quantity - remaining;
1695 let side = if o.side == "B" {
1696 OrderSide::Buy
1697 } else {
1698 OrderSide::Sell
1699 };
1700 Some(OpenOrder {
1701 order_id: o.oid,
1702 symbol: o.coin,
1703 side,
1704 price,
1705 quantity,
1706 filled_quantity,
1707 order_type: None, trigger_price: None, reduce_only: false, })
1711 })
1712 .collect())
1713 }
1714
1715 async fn get_collateral(&self) -> Result<Decimal, String> {
1717 let user = self.require_user_address()?;
1718 let resp = self
1720 .info_post(
1721 serde_json::json!({"type": "clearinghouseState", "user": user}),
1722 2,
1723 "get_collateral",
1724 )
1725 .await?;
1726 let state: ClearinghouseStateResponse = parse_response(resp).await?;
1727 parse_decimal(&state.margin_summary.account_value)
1728 .ok_or_else(|| "invalid account value".to_string())
1729 }
1730
1731 async fn get_spot_balance(&self) -> Result<Vec<guilder_abstraction::Balance>, String> {
1733 let user = self.require_user_address()?;
1734 let resp = self
1736 .info_post(
1737 serde_json::json!({"type": "spotClearinghouseState", "user": user}),
1738 15,
1739 "get_spot_balance",
1740 )
1741 .await?;
1742
1743 #[derive(Deserialize)]
1744 struct SpotStateResponse {
1745 balances: Vec<SpotBalance>,
1746 }
1747
1748 #[allow(dead_code)]
1749 #[derive(Deserialize)]
1750 struct SpotBalance {
1751 coin: String,
1752 total: String,
1753 hold: String,
1754 #[serde(default)]
1755 token: Option<i32>,
1756 #[serde(default)]
1757 #[serde(rename = "entryNtl")]
1758 entry_ntl: Option<String>,
1759 }
1760
1761 let state: SpotStateResponse = parse_response(resp).await?;
1762
1763 state
1764 .balances
1765 .into_iter()
1766 .map(|balance| {
1767 let total = parse_decimal(&balance.total)
1768 .ok_or_else(|| "invalid total balance".to_string())?;
1769 let locked = parse_decimal(&balance.hold)
1770 .ok_or_else(|| "invalid hold balance".to_string())?;
1771 let available = total - locked;
1772
1773 Ok(guilder_abstraction::Balance {
1774 coin: balance.coin,
1775 total,
1776 available,
1777 locked,
1778 })
1779 })
1780 .collect()
1781 }
1782
1783 async fn get_collateral_balance(
1786 &self,
1787 asset: String,
1788 ) -> Result<guilder_abstraction::Balance, String> {
1789 if asset.to_uppercase() != "USDC" {
1790 return Err(format!("only USDC collateral is supported, got {}", asset));
1791 }
1792
1793 let total = self.get_collateral().await?;
1794 Ok(guilder_abstraction::Balance {
1795 coin: "USDC".to_string(),
1796 total,
1797 available: total,
1798 locked: Decimal::ZERO,
1799 })
1800 }
1801
1802 async fn get_user_rate_limit(&self) -> Result<guilder_abstraction::UserRateLimit, String> {
1805 let user = self.require_user_address()?;
1806 let resp = self
1807 .info_post(
1808 serde_json::json!({"type": "userRateLimit", "user": user}),
1809 20,
1810 "get_user_rate_limit",
1811 )
1812 .await?;
1813 let val = parse_response::<Value>(resp).await?;
1814
1815 let cumulative_volume = val["cumVlm"]
1816 .as_str()
1817 .and_then(parse_decimal)
1818 .ok_or_else(|| "missing or invalid cumVlm".to_string())?;
1819 let requests_used = val["nRequestsUsed"]
1820 .as_i64()
1821 .ok_or_else(|| "missing or invalid nRequestsUsed".to_string())?;
1822 let requests_cap = val["nRequestsCap"]
1823 .as_i64()
1824 .ok_or_else(|| "missing or invalid nRequestsCap".to_string())?;
1825 let requests_surplus = val["nRequestsSurplus"]
1826 .as_i64()
1827 .ok_or_else(|| "missing or invalid nRequestsSurplus".to_string())?;
1828
1829 Ok(guilder_abstraction::UserRateLimit {
1830 cumulative_volume,
1831 requests_used,
1832 requests_cap,
1833 requests_surplus,
1834 })
1835 }
1836}
1837
1838#[allow(async_fn_in_trait)]
1839impl guilder_abstraction::SubscribeUserEvents for HyperliquidClient {
1840 fn subscribe_user_fills(&self) -> BoxStream<Result<UserFill, String>> {
1841 let Some(addr) = self.user_address.as_ref() else {
1842 return Box::pin(stream::empty());
1843 };
1844 let addr_str = addr.clone();
1845 let sub = serde_json::json!({
1846 "method": "subscribe",
1847 "subscription": {"type": "userEvents", "user": addr_str.clone()}
1848 });
1849 let key = crate::ws::SubKey {
1850 channel: "user".to_string(),
1851 routing_key: addr_str,
1852 };
1853 let raw_stream = self.ws_mux.subscribe(key, sub);
1854 Box::pin(
1855 raw_stream
1856 .filter_map(|text| async move {
1857 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1858 return None;
1859 };
1860 if env.channel != "user" {
1861 return None;
1862 }
1863 let Ok(event) = serde_json::from_value::<WsUserEvent>(env.data) else {
1864 return None;
1865 };
1866 let items: Vec<_> = event
1867 .fills
1868 .unwrap_or_default()
1869 .into_iter()
1870 .filter_map(|fill| {
1871 let side = if fill.side == "B" {
1872 OrderSide::Buy
1873 } else {
1874 OrderSide::Sell
1875 };
1876 let price = parse_decimal(&fill.px)?;
1877 let quantity = parse_decimal(&fill.sz)?;
1878 let fee_usd = parse_decimal(&fill.fee)?;
1879 Some(UserFill {
1880 order_id: fill.oid,
1881 symbol: fill.coin,
1882 side,
1883 price,
1884 quantity,
1885 fee_usd,
1886 timestamp_ms: fill.time,
1887 cloid: fill.cloid,
1888 })
1889 })
1890 .collect();
1891 if items.is_empty() {
1892 None
1893 } else {
1894 Some(stream::iter(items.into_iter().map(Ok)))
1895 }
1896 })
1897 .flatten(),
1898 )
1899 }
1900
1901 fn subscribe_order_updates(&self) -> BoxStream<Result<OrderUpdate, String>> {
1902 let Some(addr) = self.user_address.as_ref() else {
1903 return Box::pin(stream::empty());
1904 };
1905 let addr_str = addr.clone();
1906 let sub = serde_json::json!({
1907 "method": "subscribe",
1908 "subscription": {"type": "orderUpdates", "user": addr_str.clone()}
1909 });
1910 let key = crate::ws::SubKey {
1911 channel: "orderUpdates".to_string(),
1912 routing_key: addr_str,
1913 };
1914 let raw_stream = self.ws_mux.subscribe(key, sub);
1915 Box::pin(
1916 raw_stream
1917 .filter_map(|text| async move {
1918 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1919 return None;
1920 };
1921 if env.channel != "orderUpdates" {
1922 return None;
1923 }
1924 let Ok(updates) = serde_json::from_value::<Vec<WsOrderUpdate>>(env.data) else {
1925 return None;
1926 };
1927 let items: Vec<_> = updates
1928 .into_iter()
1929 .map(|upd| {
1930 let status = match upd.status.as_str() {
1931 "open" => OrderStatus::Placed,
1932 "filled" => OrderStatus::Filled,
1933 "canceled" | "cancelled" => OrderStatus::Cancelled,
1934 _ => OrderStatus::PartiallyFilled,
1935 };
1936 let side = if upd.order.side == "B" {
1937 OrderSide::Buy
1938 } else {
1939 OrderSide::Sell
1940 };
1941 OrderUpdate {
1942 order_id: upd.order.oid,
1943 symbol: upd.order.coin,
1944 status,
1945 side: Some(side),
1946 price: parse_decimal(&upd.order.limit_px),
1947 quantity: parse_decimal(&upd.order.orig_sz),
1948 remaining_quantity: parse_decimal(&upd.order.sz),
1949 timestamp_ms: upd.status_timestamp,
1950 cloid: upd.order.cloid,
1951 }
1952 })
1953 .collect();
1954 if items.is_empty() {
1955 None
1956 } else {
1957 Some(stream::iter(items.into_iter().map(Ok)))
1958 }
1959 })
1960 .flatten(),
1961 )
1962 }
1963
1964 fn subscribe_funding_payments(&self) -> BoxStream<Result<FundingPayment, String>> {
1965 let Some(addr) = self.user_address.as_ref() else {
1966 return Box::pin(stream::empty());
1967 };
1968 let addr_str = addr.clone();
1969 let sub = serde_json::json!({
1970 "method": "subscribe",
1971 "subscription": {"type": "userEvents", "user": addr_str.clone()}
1972 });
1973 let key = crate::ws::SubKey {
1974 channel: "user".to_string(),
1975 routing_key: addr_str,
1976 };
1977 let raw_stream = self.ws_mux.subscribe(key, sub);
1978 Box::pin(
1979 raw_stream
1980 .filter_map(|text| async move {
1981 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1982 return None;
1983 };
1984 if env.channel != "user" {
1985 return None;
1986 }
1987 let Ok(event) = serde_json::from_value::<WsUserEvent>(env.data) else {
1988 return None;
1989 };
1990 let funding = event.funding?;
1991 let amount_usd = parse_decimal(&funding.usdc)?;
1992 let item = FundingPayment {
1993 symbol: funding.coin,
1994 amount_usd,
1995 timestamp_ms: funding.time,
1996 };
1997 Some(stream::iter(vec![Ok(item)]))
1998 })
1999 .flatten(),
2000 )
2001 }
2002
2003 fn subscribe_deposits(&self) -> BoxStream<Result<Deposit, String>> {
2004 let Some(addr) = self.user_address.as_ref() else {
2005 return Box::pin(stream::empty());
2006 };
2007 let addr_str = addr.clone();
2008 let sub = serde_json::json!({
2009 "method": "subscribe",
2010 "subscription": {"type": "userNonFundingLedgerUpdates", "user": addr_str.clone()}
2011 });
2012 let key = crate::ws::SubKey {
2013 channel: "userNonFundingLedgerUpdates".to_string(),
2014 routing_key: addr_str,
2015 };
2016 let raw_stream = self.ws_mux.subscribe(key, sub);
2017 Box::pin(
2018 raw_stream
2019 .filter_map(|text| async move {
2020 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
2021 return None;
2022 };
2023 if env.channel != "userNonFundingLedgerUpdates" {
2024 return None;
2025 }
2026 let Ok(ledger) = serde_json::from_value::<WsLedgerUpdates>(env.data) else {
2027 return None;
2028 };
2029 let items: Vec<_> = ledger
2030 .updates
2031 .into_iter()
2032 .filter_map(|e| {
2033 if e.delta.kind != "deposit" {
2034 return None;
2035 }
2036 let amount_usd = e.delta.usdc.as_deref().and_then(parse_decimal)?;
2037 Some(Deposit {
2038 asset: "USDC".to_string(),
2039 amount_usd,
2040 timestamp_ms: e.time,
2041 })
2042 })
2043 .collect();
2044 if items.is_empty() {
2045 None
2046 } else {
2047 Some(stream::iter(items.into_iter().map(Ok)))
2048 }
2049 })
2050 .flatten(),
2051 )
2052 }
2053
2054 fn subscribe_withdrawals(&self) -> BoxStream<Result<Withdrawal, String>> {
2055 let Some(addr) = self.user_address.as_ref() else {
2056 return Box::pin(stream::empty());
2057 };
2058 let addr_str = addr.clone();
2059 let sub = serde_json::json!({
2060 "method": "subscribe",
2061 "subscription": {"type": "userNonFundingLedgerUpdates", "user": addr_str.clone()}
2062 });
2063 let key = crate::ws::SubKey {
2064 channel: "userNonFundingLedgerUpdates".to_string(),
2065 routing_key: addr_str,
2066 };
2067 let raw_stream = self.ws_mux.subscribe(key, sub);
2068 Box::pin(
2069 raw_stream
2070 .filter_map(|text| async move {
2071 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
2072 return None;
2073 };
2074 if env.channel != "userNonFundingLedgerUpdates" {
2075 return None;
2076 }
2077 let Ok(ledger) = serde_json::from_value::<WsLedgerUpdates>(env.data) else {
2078 return None;
2079 };
2080 let items: Vec<_> = ledger
2081 .updates
2082 .into_iter()
2083 .filter_map(|e| {
2084 if e.delta.kind != "withdraw" {
2085 return None;
2086 }
2087 let amount_usd = e.delta.usdc.as_deref().and_then(parse_decimal)?;
2088 Some(Withdrawal {
2089 asset: "USDC".to_string(),
2090 amount_usd,
2091 timestamp_ms: e.time,
2092 })
2093 })
2094 .collect();
2095 if items.is_empty() {
2096 None
2097 } else {
2098 Some(stream::iter(items.into_iter().map(Ok)))
2099 }
2100 })
2101 .flatten(),
2102 )
2103 }
2104
2105 fn subscribe_spot_balance(
2108 &self,
2109 ) -> BoxStream<Result<Vec<guilder_abstraction::Balance>, String>> {
2110 let Some(addr) = self.user_address.as_ref() else {
2111 return Box::pin(stream::iter(vec![Err(
2112 "user address not registered".to_string()
2113 )]));
2114 };
2115 let addr_str = addr.clone();
2116 self.subscribe_spot_balance_with_address(addr_str)
2117 }
2118
2119 fn subscribe_spot_balance_with_address(
2121 &self,
2122 address: String,
2123 ) -> BoxStream<Result<Vec<guilder_abstraction::Balance>, String>> {
2124 let addr_str = address;
2125 let sub = serde_json::json!({
2126 "method": "subscribe",
2127 "subscription": {"type": "userEvents", "user": addr_str.clone()}
2128 });
2129 let key = crate::ws::SubKey {
2130 channel: "user".to_string(),
2131 routing_key: addr_str,
2132 };
2133 let raw_stream = self.ws_mux.subscribe(key, sub);
2134 Box::pin(raw_stream.filter_map(|text| async move {
2135 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
2136 return None;
2137 };
2138 if env.channel != "user" {
2139 return None;
2140 }
2141 let Ok(event) = serde_json::from_value::<WsUserEvent>(env.data) else {
2142 return None;
2143 };
2144
2145 let spot_state = event.spot_state?;
2147 let balances = spot_state.balances?;
2148
2149 let items: Vec<_> = balances
2150 .into_iter()
2151 .filter_map(|b| {
2152 let total = parse_decimal(&b.total)?;
2153 let locked = parse_decimal(&b.hold)?;
2154 let available = total - locked;
2155 Some(guilder_abstraction::Balance {
2156 coin: b.coin,
2157 total,
2158 available,
2159 locked,
2160 })
2161 })
2162 .collect();
2163
2164 if items.is_empty() {
2165 None
2166 } else {
2167 Some(Ok(items))
2168 }
2169 }))
2170 }
2171}
2172
2173#[cfg(test)]
2174mod msgpack_tests {
2175 use super::*;
2176 use serde_json::json;
2177
2178 #[test]
2179 fn test_msgpack_null() {
2180 let result = value_to_msgpack(&Value::Null);
2181 assert_eq!(result, vec![0xc0]);
2182 }
2183
2184 #[test]
2185 fn test_msgpack_bool() {
2186 assert_eq!(value_to_msgpack(&Value::Bool(true)), vec![0xc3]);
2187 assert_eq!(value_to_msgpack(&Value::Bool(false)), vec![0xc2]);
2188 }
2189
2190 #[test]
2191 fn test_msgpack_positive_fixint() {
2192 assert_eq!(value_to_msgpack(&json!(0)), vec![0x00]);
2194 assert_eq!(value_to_msgpack(&json!(1)), vec![0x01]);
2195 assert_eq!(value_to_msgpack(&json!(127)), vec![0x7f]);
2196 }
2197
2198 #[test]
2199 fn test_msgpack_uint8() {
2200 assert_eq!(value_to_msgpack(&json!(128)), vec![0xcc, 0x80]);
2202 assert_eq!(value_to_msgpack(&json!(255)), vec![0xcc, 0xff]);
2203 }
2204
2205 #[test]
2206 fn test_msgpack_uint16() {
2207 assert_eq!(value_to_msgpack(&json!(256)), vec![0xcd, 0x01, 0x00]);
2209 assert_eq!(value_to_msgpack(&json!(65535)), vec![0xcd, 0xff, 0xff]);
2210 }
2211
2212 #[test]
2213 fn test_msgpack_uint32() {
2214 assert_eq!(
2216 value_to_msgpack(&json!(65536)),
2217 vec![0xce, 0x00, 0x01, 0x00, 0x00]
2218 );
2219 assert_eq!(
2220 value_to_msgpack(&json!(4294967295u64)),
2221 vec![0xce, 0xff, 0xff, 0xff, 0xff]
2222 );
2223 }
2224
2225 #[test]
2226 fn test_msgpack_uint64() {
2227 let big: u64 = 4294967296;
2229 assert_eq!(
2230 value_to_msgpack(&json!(big)),
2231 vec![0xcf, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]
2232 );
2233 }
2234
2235 #[test]
2236 fn test_msgpack_negative_fixint() {
2237 assert_eq!(value_to_msgpack(&json!(-1)), vec![0xff]);
2239 assert_eq!(value_to_msgpack(&json!(-32)), vec![0xe0]);
2240 }
2241
2242 #[test]
2243 fn test_msgpack_int8() {
2244 assert_eq!(value_to_msgpack(&json!(-33)), vec![0xd0, 0xdf]);
2246 assert_eq!(value_to_msgpack(&json!(-128)), vec![0xd0, 0x80]);
2247 }
2248
2249 #[test]
2250 fn test_msgpack_int16() {
2251 assert_eq!(value_to_msgpack(&json!(-129)), vec![0xd1, 0xff, 0x7f]);
2253 assert_eq!(value_to_msgpack(&json!(-32768)), vec![0xd1, 0x80, 0x00]);
2254 }
2255
2256 #[test]
2257 fn test_msgpack_int32() {
2258 assert_eq!(
2260 value_to_msgpack(&json!(-32769)),
2261 vec![0xd2, 0xff, 0xff, 0x7f, 0xff]
2262 );
2263 assert_eq!(
2264 value_to_msgpack(&json!(-2147483648i64)),
2265 vec![0xd2, 0x80, 0x00, 0x00, 0x00]
2266 );
2267 }
2268
2269 #[test]
2270 fn test_msgpack_int64() {
2271 let val: i64 = -2147483649;
2272 let result = value_to_msgpack(&json!(val));
2273 assert_eq!(result[0], 0xd3); assert_eq!(result.len(), 9);
2275 }
2276
2277 #[test]
2278 fn test_msgpack_float() {
2279 let result = value_to_msgpack(&json!(3.14));
2280 assert_eq!(result[0], 0xcb); assert_eq!(result.len(), 9);
2282 }
2283
2284 #[test]
2285 fn test_msgpack_fixstr() {
2286 assert_eq!(value_to_msgpack(&json!("")), vec![0xa0]);
2288 assert_eq!(
2289 value_to_msgpack(&json!("hello")),
2290 {
2291 let mut expected = vec![0xa5];
2292 expected.extend_from_slice(b"hello");
2293 expected
2294 }
2295 );
2296 let s = "a".repeat(31);
2297 let result = value_to_msgpack(&json!(s));
2298 assert_eq!(result[0], 0xbf); assert_eq!(result.len(), 32);
2300 }
2301
2302 #[test]
2303 fn test_msgpack_str8() {
2304 let s = "a".repeat(32);
2305 let result = value_to_msgpack(&json!(s));
2306 assert_eq!(result[0], 0xd9); assert_eq!(result[1], 32);
2308 assert_eq!(result.len(), 34);
2309 }
2310
2311 #[test]
2312 fn test_msgpack_fixarray() {
2313 assert_eq!(value_to_msgpack(&json!([])), vec![0x90]);
2315 let result = value_to_msgpack(&json!([1, 2, 3]));
2316 assert_eq!(result[0], 0x93);
2317 assert_eq!(result, vec![0x93, 0x01, 0x02, 0x03]);
2318 }
2319
2320 #[test]
2321 fn test_msgpack_fixmap() {
2322 assert_eq!(value_to_msgpack(&json!({})), vec![0x80]);
2324 let result = value_to_msgpack(&json!({"a": 1}));
2325 assert_eq!(result[0], 0x81); assert_eq!(result, {
2327 let mut expected = vec![0x81];
2328 expected.extend_from_slice(&value_to_msgpack(&json!("a")));
2329 expected.extend_from_slice(&value_to_msgpack(&json!(1)));
2330 expected
2331 });
2332 }
2333
2334 #[test]
2335 fn test_msgmap_preserves_insertion_order() {
2336 let val = json!({
2338 "z": 1,
2339 "a": 2,
2340 "m": 3
2341 });
2342 let result = value_to_msgpack(&val);
2343 assert_eq!(result[0], 0x83);
2345 assert_eq!(result[1], 0xa1); assert_eq!(result[2], b'z');
2348 }
2349
2350 #[test]
2351 fn test_msgpack_mixed_array() {
2352 let val = json!([null, true, false, 42, "hi", [1, 2]]);
2353 let result = value_to_msgpack(&val);
2354 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);
2361 assert_eq!(result[6], b'h');
2362 assert_eq!(result[7], b'i');
2363 }
2364
2365 #[test]
2366 fn test_build_order_msgpack_without_cloid() {
2367 let result = build_order_msgpack(
2368 0, true, "1000", "0.1", false, "limit", b"gtc", None, );
2377 assert_eq!(result[0], 0x86);
2379 }
2380
2381 #[test]
2382 fn test_build_order_msgpack_with_cloid() {
2383 let result = build_order_msgpack(
2384 0, true, "1000", "0.1", false, "limit", b"gtc", Some("my-cloid"), );
2393 assert_eq!(result[0], 0x87);
2395 }
2396
2397 #[test]
2398 fn test_action_to_canonical_msgpack() {
2399 let action = json!({
2400 "type": "order",
2401 "orders": [{"a": 0, "b": true, "p": "1000", "s": "0.1", "r": false, "t": {"limit": {"tif": "gtc"}}}],
2402 "grouping": "na"
2403 });
2404 let result = action_to_canonical_msgpack(&action).unwrap();
2405 assert_eq!(result[0], 0x83);
2407 }
2408
2409 #[test]
2410 fn test_msgpack_matches_rmp_serde_for_simple_values() {
2411 use rmp_serde::to_vec;
2413
2414 for val in [json!(0), json!(127), json!(255), json!(1000), json!(-1), json!(-32), json!(-128)] {
2415 let ours = value_to_msgpack(&val);
2416 let theirs = to_vec(&val).unwrap();
2417 assert_eq!(
2418 ours, theirs,
2419 "mismatch for {}: ours={:?}, rmp={:?}",
2420 val, ours, theirs
2421 );
2422 }
2423 }
2424
2425 #[test]
2426 fn test_msgpack_string_encoding() {
2427 use rmp_serde::to_vec;
2428 for val in [json!(""), json!("a"), json!("hello world"), json!("BTC-USD")] {
2429 let ours = value_to_msgpack(&val);
2430 let theirs = to_vec(&val).unwrap();
2431 assert_eq!(
2432 ours, theirs,
2433 "mismatch for {}: ours={:?}, rmp={:?}",
2434 val, ours, theirs
2435 );
2436 }
2437 }
2438
2439 #[test]
2440 fn test_msgpack_bool_encoding() {
2441 use rmp_serde::to_vec;
2442 let theirs = to_vec(&json!(true)).unwrap();
2443 assert_eq!(value_to_msgpack(&json!(true)), theirs);
2444 let theirs = to_vec(&json!(false)).unwrap();
2445 assert_eq!(value_to_msgpack(&json!(false)), theirs);
2446 }
2447
2448 #[test]
2449 fn test_msgpack_null_encoding() {
2450 use rmp_serde::to_vec;
2451 let theirs = to_vec(&Value::Null).unwrap();
2452 assert_eq!(value_to_msgpack(&Value::Null), theirs);
2453 }
2454
2455 #[test]
2456 fn test_msgpack_nested_object() {
2457 let val = json!({
2458 "outer": {
2459 "inner": 42
2460 }
2461 });
2462 let result = value_to_msgpack(&val);
2463 assert_eq!(result[0], 0x81); }
2465
2466 #[test]
2467 fn test_msgpack_empty_containers() {
2468 assert_eq!(value_to_msgpack(&json!([])), vec![0x90]);
2469 assert_eq!(value_to_msgpack(&json!({})), vec![0x80]);
2470 }
2471}