Skip to main content

guilder_client_hyperliquid/
client.rs

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