1use crate::rate_limiter::{RestRateLimiter, AddressRateLimiter};
2use alloy_primitives::Address;
3use futures_util::{stream, StreamExt};
4use guilder_abstraction::{
5 self, AssetContext, BoxStream, Deposit, Fill, FundingPayment, L2Update, Liquidation, OpenOrder,
6 OrderPlacement, OrderSide, OrderStatus, OrderType, OrderUpdate, Position, PredictedFunding,
7 Side, TimeInForce, UserFill, Withdrawal,
8};
9use reqwest::Client;
10use rust_decimal::Decimal;
11use serde::Deserialize;
12use serde_json::Value;
13use std::collections::HashMap;
14use std::str::FromStr;
15use std::sync::Arc;
16const HYPERLIQUID_INFO_URL: &str = "https://api.hyperliquid.xyz/info";
17const HYPERLIQUID_EXCHANGE_URL: &str = "https://api.hyperliquid.xyz/exchange";
18
19async fn parse_response<T: for<'de> serde::Deserialize<'de>>(
20 resp: reqwest::Response,
21) -> Result<T, String> {
22 let text = resp.text().await.map_err(|e| e.to_string())?;
23 serde_json::from_str(&text).map_err(|e| format!("{e}: {text}"))
24}
25
26pub struct HyperliquidClient {
27 client: Client,
28 user_address: Option<Address>,
29 private_key: Option<String>,
30 rest_limiter: Arc<RestRateLimiter>,
31 address_limiter: Arc<AddressRateLimiter>,
32 ws_mux: crate::ws::WsMux,
33}
34
35impl Default for HyperliquidClient {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl HyperliquidClient {
42 pub fn new() -> Self {
43 HyperliquidClient {
44 client: Client::new(),
45 user_address: None,
46 private_key: None,
47 rest_limiter: Arc::new(RestRateLimiter::new()),
48 address_limiter: Arc::new(AddressRateLimiter::new()),
49 ws_mux: crate::ws::WsMux::new(),
50 }
51 }
52
53 pub fn with_auth(user_address: Address, private_key: String) -> Self {
54 HyperliquidClient {
55 client: Client::new(),
56 user_address: Some(user_address),
57 private_key: Some(private_key),
58 rest_limiter: Arc::new(RestRateLimiter::new()),
59 address_limiter: Arc::new(AddressRateLimiter::new()),
60 ws_mux: crate::ws::WsMux::new(),
61 }
62 }
63
64 pub fn with_budgets(mut self, rest_weight: u32, addr_budget: u64) -> Self {
67 self.rest_limiter = Arc::new(RestRateLimiter::new_with_budget(rest_weight));
68 self.address_limiter = Arc::new(AddressRateLimiter::new_with_budget(addr_budget));
69 self
70 }
71
72 async fn info_post(&self, body: Value, weight: u32, call: &str) -> Result<reqwest::Response, String> {
74 self.rest_limiter.acquire_blocking(weight, call).await;
75 self.client
76 .post(HYPERLIQUID_INFO_URL)
77 .json(&body)
78 .send()
79 .await
80 .map_err(|e| e.to_string())
81 }
82
83 async fn exchange_post(&self, body: Value, weight: u32, call: &str) -> Result<reqwest::Response, String> {
86 self.rest_limiter.acquire_blocking(weight, call).await;
87 self.client
88 .post(HYPERLIQUID_EXCHANGE_URL)
89 .json(&body)
90 .send()
91 .await
92 .map_err(|e| e.to_string())
93 }
94
95 fn require_user_address(&self) -> Result<String, String> {
96 self.user_address
97 .map(|a| format!("{:#x}", a))
98 .ok_or_else(|| "user address required: use HyperliquidClient::with_auth".to_string())
99 }
100
101 fn require_private_key(&self) -> Result<&str, String> {
102 self.private_key
103 .as_deref()
104 .ok_or_else(|| "private key required: use HyperliquidClient::with_auth".to_string())
105 }
106
107 async fn get_asset_index(&self, symbol: &str) -> Result<usize, String> {
108 let resp = self
110 .info_post(serde_json::json!({"type": "meta"}), 20, "get_asset_index")
111 .await?;
112 let meta: MetaResponse = parse_response(resp).await?;
113 meta.universe
114 .iter()
115 .position(|a| a.name == symbol)
116 .ok_or_else(|| format!("symbol {} not found", symbol))
117 }
118
119 async fn submit_signed_action(
120 &self,
121 action: Value,
122 vault_address: Option<&str>,
123 ) -> Result<Value, String> {
124 let private_key = self.require_private_key()?;
125 let nonce = std::time::SystemTime::now()
126 .duration_since(std::time::UNIX_EPOCH)
127 .unwrap()
128 .as_millis() as u64;
129
130 let (r, s, v) = sign_action(private_key, &action, vault_address, nonce)?;
131
132 let payload = serde_json::json!({
133 "action": action,
134 "nonce": nonce,
135 "signature": {"r": r, "s": s, "v": v},
136 "vaultAddress": null
137 });
138
139 let resp = self.exchange_post(payload, 1, "submit_signed_action").await?;
141
142 let body: Value = parse_response(resp).await?;
143 if body["status"].as_str() == Some("err") {
144 return Err(body["response"]
145 .as_str()
146 .unwrap_or("unknown error")
147 .to_string());
148 }
149 Ok(body)
150 }
151}
152
153#[derive(Deserialize)]
156struct MetaResponse {
157 universe: Vec<AssetInfo>,
158}
159
160#[derive(Deserialize)]
161struct AssetInfo {
162 name: String,
163}
164
165type MetaAndAssetCtxsResponse = (MetaResponse, Vec<RestAssetCtx>);
166
167#[derive(Deserialize)]
168#[serde(rename_all = "camelCase")]
169#[allow(dead_code)]
170struct RestAssetCtx {
171 open_interest: String,
172 funding: String,
173 mark_px: String,
174 day_ntl_vlm: String,
175 mid_px: Option<String>,
176 oracle_px: Option<String>,
177 premium: Option<String>,
178 prev_day_px: Option<String>,
179}
180
181#[derive(Deserialize)]
182#[serde(rename_all = "camelCase")]
183struct ClearinghouseStateResponse {
184 margin_summary: MarginSummary,
185 asset_positions: Vec<AssetPosition>,
186}
187
188#[derive(Deserialize)]
189#[serde(rename_all = "camelCase")]
190struct MarginSummary {
191 account_value: String,
192}
193
194#[derive(Deserialize)]
195struct AssetPosition {
196 position: PositionDetail,
197}
198
199#[derive(Deserialize)]
200#[serde(rename_all = "camelCase")]
201struct PositionDetail {
202 coin: String,
203 szi: String,
205 entry_px: Option<String>,
206}
207
208#[derive(Deserialize)]
209#[serde(rename_all = "camelCase")]
210struct RestOpenOrder {
211 coin: String,
212 side: String,
213 limit_px: String,
214 sz: String,
215 oid: i64,
216 orig_sz: String,
217}
218
219type PredictedFundingsResponse = Vec<(String, Vec<(String, Option<PredictedFundingEntry>)>)>;
222
223#[derive(Deserialize)]
224#[serde(rename_all = "camelCase")]
225struct PredictedFundingEntry {
226 funding_rate: String,
227 next_funding_time: i64,
228}
229
230#[derive(Deserialize)]
233struct WsEnvelope {
234 channel: String,
235 #[serde(default)]
236 data: Value,
237}
238
239#[derive(Deserialize)]
240struct WsBook {
241 coin: String,
242 levels: Vec<Vec<WsLevel>>,
243 time: i64,
244}
245
246#[derive(Deserialize)]
247struct WsLevel {
248 px: String,
249 sz: String,
250}
251
252#[derive(Deserialize)]
253#[serde(rename_all = "camelCase")]
254struct WsAssetCtx {
255 coin: String,
256 ctx: WsPerpsCtx,
257}
258
259#[derive(Deserialize)]
260#[serde(rename_all = "camelCase")]
261struct WsPerpsCtx {
262 open_interest: String,
263 funding: String,
264 mark_px: String,
265 day_ntl_vlm: String,
266 mid_px: Option<String>,
267 oracle_px: Option<String>,
268 premium: Option<String>,
269 prev_day_px: Option<String>,
270}
271
272#[derive(Deserialize)]
273struct WsUserEvent {
274 liquidation: Option<WsLiquidation>,
275 fills: Option<Vec<WsUserFill>>,
276 funding: Option<WsFunding>,
277}
278
279#[derive(Deserialize)]
280struct WsLiquidation {
281 liquidated_user: String,
282 liquidated_ntl_pos: String,
283 liquidated_account_value: String,
284}
285
286#[derive(Deserialize)]
287struct WsUserFill {
288 coin: String,
289 px: String,
290 sz: String,
291 side: String,
292 time: i64,
293 oid: i64,
294 fee: String,
295}
296
297#[derive(Deserialize)]
298struct WsFunding {
299 time: i64,
300 coin: String,
301 usdc: String,
302}
303
304#[derive(Deserialize)]
305struct WsTrade {
306 coin: String,
307 side: String,
308 px: String,
309 sz: String,
310 time: i64,
311 tid: i64,
312}
313
314#[derive(Deserialize)]
315struct WsOrderUpdate {
316 order: WsOrderInfo,
317 status: String,
318 #[serde(rename = "statusTimestamp")]
319 status_timestamp: i64,
320}
321
322#[derive(Deserialize)]
323#[serde(rename_all = "camelCase")]
324struct WsOrderInfo {
325 coin: String,
326 side: String,
327 limit_px: String,
328 sz: String,
329 oid: i64,
330 orig_sz: String,
331}
332
333#[derive(Deserialize)]
336struct WsLedgerUpdates {
337 updates: Vec<WsLedgerEntry>,
338}
339
340#[derive(Deserialize)]
341struct WsLedgerEntry {
342 time: i64,
343 delta: WsLedgerDelta,
344}
345
346#[derive(Deserialize)]
347struct WsLedgerDelta {
348 #[serde(rename = "type")]
349 kind: String,
350 usdc: Option<String>,
351}
352
353fn parse_decimal(s: &str) -> Option<Decimal> {
356 Decimal::from_str(s).ok()
357}
358
359fn keccak256(data: &[u8]) -> [u8; 32] {
360 use sha3::{Digest, Keccak256};
361 Keccak256::digest(data).into()
362}
363
364fn hyperliquid_domain_separator() -> [u8; 32] {
366 let type_hash = keccak256(
367 b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)",
368 );
369 let name_hash = keccak256(b"Exchange");
370 let version_hash = keccak256(b"1");
371 let mut chain_id = [0u8; 32];
372 chain_id[28..32].copy_from_slice(&42161u32.to_be_bytes());
373 let verifying_contract = [0u8; 32];
374
375 let mut data = [0u8; 160];
376 data[..32].copy_from_slice(&type_hash);
377 data[32..64].copy_from_slice(&name_hash);
378 data[64..96].copy_from_slice(&version_hash);
379 data[96..128].copy_from_slice(&chain_id);
380 data[128..160].copy_from_slice(&verifying_contract);
381 keccak256(&data)
382}
383
384fn sign_action(
387 private_key: &str,
388 action: &Value,
389 vault_address: Option<&str>,
390 nonce: u64,
391) -> Result<(String, String, u8), String> {
392 use k256::ecdsa::SigningKey;
393
394 let msgpack_bytes = rmp_serde::to_vec(action).map_err(|e| e.to_string())?;
396 let mut data = msgpack_bytes;
397 data.extend_from_slice(&nonce.to_be_bytes());
398 match vault_address {
399 None => data.push(0u8),
400 Some(addr) => {
401 data.push(1u8);
402 let addr_bytes = hex::decode(addr.trim_start_matches("0x"))
403 .map_err(|e| format!("invalid vault address: {}", e))?;
404 data.extend_from_slice(&addr_bytes);
405 }
406 }
407 let connection_id = keccak256(&data);
408
409 let agent_type_hash = keccak256(b"Agent(string source,bytes32 connectionId)");
411 let source_hash = keccak256(b"a"); let mut struct_data = [0u8; 96];
413 struct_data[..32].copy_from_slice(&agent_type_hash);
414 struct_data[32..64].copy_from_slice(&source_hash);
415 struct_data[64..96].copy_from_slice(&connection_id);
416 let struct_hash = keccak256(&struct_data);
417
418 let domain_sep = hyperliquid_domain_separator();
420 let mut final_data = Vec::with_capacity(66);
421 final_data.extend_from_slice(b"\x19\x01");
422 final_data.extend_from_slice(&domain_sep);
423 final_data.extend_from_slice(&struct_hash);
424 let final_hash = keccak256(&final_data);
425
426 let key_bytes = hex::decode(private_key.trim_start_matches("0x"))
428 .map_err(|e| format!("invalid private key: {}", e))?;
429 let signing_key =
430 SigningKey::from_bytes(key_bytes.as_slice().into()).map_err(|e| e.to_string())?;
431 let (sig, recovery_id) = signing_key
432 .sign_prehash_recoverable(&final_hash)
433 .map_err(|e| e.to_string())?;
434
435 let sig_bytes = sig.to_bytes();
436 let r = format!("0x{}", hex::encode(&sig_bytes[..32]));
437 let s = format!("0x{}", hex::encode(&sig_bytes[32..64]));
438 let v = 27u8 + recovery_id.to_byte();
439
440 Ok((r, s, v))
441}
442
443#[allow(async_fn_in_trait)]
446impl guilder_abstraction::TestServer for HyperliquidClient {
447 async fn ping(&self) -> Result<bool, String> {
449 self.info_post(serde_json::json!({"type": "allMids"}), 2, "ping")
451 .await
452 .map(|r| r.status().is_success())
453 }
454
455 async fn get_server_time(&self) -> Result<i64, String> {
457 Ok(std::time::SystemTime::now()
458 .duration_since(std::time::UNIX_EPOCH)
459 .map(|d| d.as_millis() as i64)
460 .unwrap_or(0))
461 }
462}
463
464#[allow(async_fn_in_trait)]
465impl guilder_abstraction::GetMarketData for HyperliquidClient {
466 async fn get_symbol(&self) -> Result<Vec<String>, String> {
468 let resp = self
470 .info_post(serde_json::json!({"type": "meta"}), 20, "get_symbol")
471 .await?;
472 parse_response::<MetaResponse>(resp)
473 .await
474 .map(|r| r.universe.into_iter().map(|a| a.name).collect())
475 }
476
477 async fn get_open_interest(&self, symbol: String) -> Result<Decimal, String> {
479 let resp = self
481 .info_post(serde_json::json!({"type": "metaAndAssetCtxs"}), 20, "get_open_interest")
482 .await?;
483 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
484 .await?
485 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
486 meta.universe
487 .iter()
488 .position(|a| a.name == symbol)
489 .and_then(|i| ctxs.get(i))
490 .and_then(|ctx| parse_decimal(&ctx.open_interest))
491 .ok_or_else(|| format!("symbol {} not found", symbol))
492 }
493
494 async fn get_asset_context(&self, symbol: String) -> Result<AssetContext, String> {
496 let resp = self
498 .info_post(serde_json::json!({"type": "metaAndAssetCtxs"}), 20, "get_asset_context")
499 .await?;
500 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
501 .await?
502 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
503 let idx = meta
504 .universe
505 .iter()
506 .position(|a| a.name == symbol)
507 .ok_or_else(|| format!("symbol {} not found", symbol))?;
508 let ctx = ctxs
509 .get(idx)
510 .ok_or_else(|| format!("symbol {} not found", symbol))?;
511 Ok(AssetContext {
512 symbol,
513 open_interest: parse_decimal(&ctx.open_interest).ok_or("invalid open_interest")?,
514 funding_rate: parse_decimal(&ctx.funding).ok_or("invalid funding")?,
515 mark_price: parse_decimal(&ctx.mark_px).ok_or("invalid mark_px")?,
516 day_volume: parse_decimal(&ctx.day_ntl_vlm).ok_or("invalid day_ntl_vlm")?,
517 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
518 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
519 premium: ctx.premium.as_deref().and_then(parse_decimal),
520 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
521 })
522 }
523
524 async fn get_all_asset_contexts(&self) -> Result<Vec<AssetContext>, String> {
527 let resp = self
529 .info_post(serde_json::json!({"type": "metaAndAssetCtxs"}), 20, "get_all_asset_contexts")
530 .await?;
531 let (meta, ctxs) = parse_response::<Option<MetaAndAssetCtxsResponse>>(resp)
532 .await?
533 .ok_or_else(|| "metaAndAssetCtxs returned null".to_string())?;
534 let mut result = Vec::with_capacity(meta.universe.len());
535 for (asset, ctx) in meta.universe.iter().zip(ctxs.iter()) {
536 let Some(open_interest) = parse_decimal(&ctx.open_interest) else {
537 continue;
538 };
539 let Some(funding_rate) = parse_decimal(&ctx.funding) else {
540 continue;
541 };
542 let Some(mark_price) = parse_decimal(&ctx.mark_px) else {
543 continue;
544 };
545 let Some(day_volume) = parse_decimal(&ctx.day_ntl_vlm) else {
546 continue;
547 };
548 result.push(AssetContext {
549 symbol: asset.name.clone(),
550 open_interest,
551 funding_rate,
552 mark_price,
553 day_volume,
554 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
555 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
556 premium: ctx.premium.as_deref().and_then(parse_decimal),
557 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
558 });
559 }
560 Ok(result)
561 }
562
563 async fn get_l2_orderbook(&self, symbol: String) -> Result<Vec<L2Update>, String> {
566 let resp = self
568 .info_post(serde_json::json!({"type": "l2Book", "coin": symbol}), 2, "get_l2_orderbook")
569 .await?;
570 let book: Option<WsBook> = parse_response(resp).await?;
571 let book = match book {
572 Some(b) => b,
573 None => return Ok(vec![]),
574 };
575 let mut levels = Vec::new();
576 for level in book.levels.first().into_iter().flatten() {
577 if let (Some(price), Some(volume)) =
578 (parse_decimal(&level.px), parse_decimal(&level.sz))
579 {
580 levels.push(L2Update {
581 symbol: book.coin.clone(),
582 price,
583 volume,
584 side: Side::Ask,
585 sequence: book.time,
586 });
587 }
588 }
589 for level in book.levels.get(1).into_iter().flatten() {
590 if let (Some(price), Some(volume)) =
591 (parse_decimal(&level.px), parse_decimal(&level.sz))
592 {
593 levels.push(L2Update {
594 symbol: book.coin.clone(),
595 price,
596 volume,
597 side: Side::Bid,
598 sequence: book.time,
599 });
600 }
601 }
602 Ok(levels)
603 }
604
605 async fn get_price(&self, symbol: String) -> Result<Decimal, String> {
607 let resp = self
609 .info_post(serde_json::json!({"type": "allMids"}), 2, "get_price")
610 .await?;
611 parse_response::<HashMap<String, String>>(resp)
612 .await?
613 .get(&symbol)
614 .and_then(|s| parse_decimal(s))
615 .ok_or_else(|| format!("symbol {} not found", symbol))
616 }
617
618 async fn get_predicted_fundings(&self) -> Result<Vec<PredictedFunding>, String> {
621 let resp = self
623 .info_post(serde_json::json!({"type": "predictedFundings"}), 20, "get_predicted_fundings")
624 .await?;
625 let data: PredictedFundingsResponse = parse_response(resp).await?;
626 let mut result = Vec::new();
627 for (symbol, venues) in data {
628 for (venue, entry) in venues {
629 let Some(entry) = entry else { continue };
630 if let Some(funding_rate) = parse_decimal(&entry.funding_rate) {
631 result.push(PredictedFunding {
632 symbol: symbol.clone(),
633 venue,
634 funding_rate,
635 next_funding_time_ms: entry.next_funding_time,
636 });
637 }
638 }
639 }
640 Ok(result)
641 }
642}
643
644#[allow(async_fn_in_trait)]
645impl guilder_abstraction::ManageOrder for HyperliquidClient {
646 async fn place_order(
649 &self,
650 symbol: String,
651 side: OrderSide,
652 price: Decimal,
653 volume: Decimal,
654 order_type: OrderType,
655 time_in_force: TimeInForce,
656 ) -> Result<OrderPlacement, String> {
657 let asset_idx = self.get_asset_index(&symbol).await?;
658 let is_buy = matches!(side, OrderSide::Buy);
659
660 let tif_str = match time_in_force {
661 TimeInForce::Gtc => "Gtc",
662 TimeInForce::Ioc => "Ioc",
663 TimeInForce::Fok => "Fok",
664 };
665 let order_type_val = match order_type {
667 OrderType::Limit => serde_json::json!({"limit": {"tif": tif_str}}),
668 OrderType::Market => serde_json::json!({"limit": {"tif": "Ioc"}}),
669 };
670
671 let action = serde_json::json!({
672 "type": "order",
673 "orders": [{
674 "a": asset_idx,
675 "b": is_buy,
676 "p": price.to_string(),
677 "s": volume.to_string(),
678 "r": false,
679 "t": order_type_val
680 }],
681 "grouping": "na"
682 });
683
684 let resp = self.submit_signed_action(action, None).await?;
685 let oid = resp["response"]["data"]["statuses"][0]["resting"]["oid"]
686 .as_i64()
687 .or_else(|| resp["response"]["data"]["statuses"][0]["filled"]["oid"].as_i64())
688 .ok_or_else(|| format!("unexpected response: {}", resp))?;
689
690 let timestamp_ms = std::time::SystemTime::now()
691 .duration_since(std::time::UNIX_EPOCH)
692 .unwrap()
693 .as_millis() as i64;
694
695 Ok(OrderPlacement {
696 order_id: oid,
697 symbol,
698 side,
699 price,
700 quantity: volume,
701 timestamp_ms,
702 })
703 }
704
705 async fn change_order_by_cloid(
708 &self,
709 cloid: i64,
710 price: Decimal,
711 volume: Decimal,
712 ) -> Result<i64, String> {
713 let user = self.require_user_address()?;
714
715 let resp = self
717 .info_post(serde_json::json!({"type": "openOrders", "user": user}), 20, "change_order_by_cloid")
718 .await?;
719 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
720 let order = orders
721 .iter()
722 .find(|o| o.oid == cloid)
723 .ok_or_else(|| format!("order {} not found", cloid))?;
724
725 let asset_idx = self.get_asset_index(&order.coin).await?;
726 let is_buy = order.side == "B";
727
728 let action = serde_json::json!({
729 "type": "batchModify",
730 "modifies": [{
731 "oid": cloid,
732 "order": {
733 "a": asset_idx,
734 "b": is_buy,
735 "p": price.to_string(),
736 "s": volume.to_string(),
737 "r": false,
738 "t": {"limit": {"tif": "Gtc"}}
739 }
740 }]
741 });
742
743 self.submit_signed_action(action, None).await?;
744 Ok(cloid)
745 }
746
747 async fn cancel_order(&self, cloid: i64) -> Result<i64, String> {
750 let user = self.require_user_address()?;
751
752 let resp = self
754 .info_post(serde_json::json!({"type": "openOrders", "user": user}), 20, "cancel_order")
755 .await?;
756 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
757 let order = orders
758 .iter()
759 .find(|o| o.oid == cloid)
760 .ok_or_else(|| format!("order {} not found", cloid))?;
761
762 let asset_idx = self.get_asset_index(&order.coin).await?;
763 let action = serde_json::json!({
764 "type": "cancel",
765 "cancels": [{"a": asset_idx, "o": cloid}]
766 });
767
768 self.submit_signed_action(action, None).await?;
769 Ok(cloid)
770 }
771
772 async fn cancel_all_order(&self) -> Result<bool, String> {
775 let user = self.require_user_address()?;
776
777 let resp = self
779 .info_post(serde_json::json!({"type": "openOrders", "user": user}), 20, "cancel_all_order")
780 .await?;
781 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
782 if orders.is_empty() {
783 return Ok(true);
784 }
785
786 let meta_resp = self
788 .info_post(serde_json::json!({"type": "meta"}), 20, "cancel_all_order")
789 .await?;
790 let meta: MetaResponse = parse_response(meta_resp).await?;
791
792 let cancels: Vec<Value> = orders
793 .iter()
794 .filter_map(|o| {
795 let asset_idx = meta.universe.iter().position(|a| a.name == o.coin)?;
796 Some(serde_json::json!({"a": asset_idx, "o": o.oid}))
797 })
798 .collect();
799
800 let action = serde_json::json!({"type": "cancel", "cancels": cancels});
801 self.submit_signed_action(action, None).await?;
802 Ok(true)
803 }
804}
805
806#[allow(async_fn_in_trait)]
807impl guilder_abstraction::SubscribeMarketData for HyperliquidClient {
808 fn subscribe_l2_update(&self, symbol: String) -> BoxStream<Result<L2Update, String>> {
809 let sub = serde_json::json!({
810 "method": "subscribe",
811 "subscription": {"type": "l2Book", "coin": symbol.clone()}
812 });
813 let key = crate::ws::SubKey {
814 channel: "l2Book".to_string(),
815 routing_key: symbol,
816 };
817 let stream = self.ws_mux.subscribe(key, sub);
818 Box::pin(async_stream::stream! {
819 for await msg in stream {
820 let Ok(env) = serde_json::from_str::<WsEnvelope>(&msg) else {
821 continue;
822 };
823 if env.channel != "l2Book" {
824 continue;
825 }
826 let Ok(book) = serde_json::from_value::<WsBook>(env.data) else {
827 continue;
828 };
829 for level in book.levels.first().into_iter().flatten() {
830 if let (Some(price), Some(volume)) =
831 (parse_decimal(&level.px), parse_decimal(&level.sz))
832 {
833 yield Ok(L2Update {
834 symbol: book.coin.clone(),
835 price,
836 volume,
837 side: Side::Ask,
838 sequence: book.time,
839 });
840 }
841 }
842 for level in book.levels.get(1).into_iter().flatten() {
843 if let (Some(price), Some(volume)) =
844 (parse_decimal(&level.px), parse_decimal(&level.sz))
845 {
846 yield Ok(L2Update {
847 symbol: book.coin.clone(),
848 price,
849 volume,
850 side: Side::Bid,
851 sequence: book.time,
852 });
853 }
854 }
855 }
856 })
857 }
858
859 fn subscribe_asset_context(&self, symbol: String) -> BoxStream<Result<AssetContext, String>> {
860 let sub = serde_json::json!({
861 "method": "subscribe",
862 "subscription": {"type": "activeAssetCtx", "coin": symbol.clone()}
863 });
864 let key = crate::ws::SubKey {
865 channel: "activeAssetCtx".to_string(),
866 routing_key: symbol,
867 };
868 let stream = self.ws_mux.subscribe(key, sub);
869 Box::pin(async_stream::stream! {
870 for await msg in stream {
871 let Ok(env) = serde_json::from_str::<WsEnvelope>(&msg) else {
872 continue;
873 };
874 if env.channel != "activeAssetCtx" {
875 continue;
876 }
877 let Ok(update) = serde_json::from_value::<WsAssetCtx>(env.data) else {
878 continue;
879 };
880 let ctx = &update.ctx;
881 let (Some(open_interest), Some(funding_rate), Some(mark_price), Some(day_volume)) = (
882 parse_decimal(&ctx.open_interest),
883 parse_decimal(&ctx.funding),
884 parse_decimal(&ctx.mark_px),
885 parse_decimal(&ctx.day_ntl_vlm),
886 ) else {
887 continue;
888 };
889 yield Ok(AssetContext {
890 symbol: update.coin,
891 open_interest,
892 funding_rate,
893 mark_price,
894 day_volume,
895 mid_price: ctx.mid_px.as_deref().and_then(parse_decimal),
896 oracle_price: ctx.oracle_px.as_deref().and_then(parse_decimal),
897 premium: ctx.premium.as_deref().and_then(parse_decimal),
898 prev_day_price: ctx.prev_day_px.as_deref().and_then(parse_decimal),
899 });
900 }
901 })
902 }
903
904 fn subscribe_liquidation(&self, user: String) -> BoxStream<Result<Liquidation, String>> {
905 let sub = serde_json::json!({
906 "method": "subscribe",
907 "subscription": {"type": "userEvents", "user": user.clone()}
908 });
909 let key = crate::ws::SubKey {
910 channel: "userEvents".to_string(),
911 routing_key: user,
912 };
913 let raw_stream = self.ws_mux.subscribe(key, sub);
914 Box::pin(raw_stream.filter_map(|text| async move {
915 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
916 return None;
917 };
918 if env.channel != "userEvents" {
919 return None;
920 }
921 let Ok(event) = serde_json::from_value::<WsUserEvent>(env.data) else {
922 return None;
923 };
924 let Some(liq) = event.liquidation else {
925 return None;
926 };
927 let (Some(notional_position), Some(account_value)) = (
928 parse_decimal(&liq.liquidated_ntl_pos),
929 parse_decimal(&liq.liquidated_account_value),
930 ) else {
931 return None;
932 };
933 let item = Liquidation {
934 symbol: String::new(),
935 side: OrderSide::Sell,
936 liquidated_user: liq.liquidated_user,
937 notional_position,
938 account_value,
939 };
940 Some(stream::iter(vec![Ok(item)].into_iter()))
941 }).flatten())
942 }
943
944 fn subscribe_fill(&self, symbol: String) -> BoxStream<Result<Fill, String>> {
945 let sub = serde_json::json!({
946 "method": "subscribe",
947 "subscription": {"type": "trades", "coin": symbol.clone()}
948 });
949 let key = crate::ws::SubKey {
950 channel: "trades".to_string(),
951 routing_key: symbol,
952 };
953 let stream = self.ws_mux.subscribe(key, sub);
954 Box::pin(async_stream::stream! {
955 for await msg in stream {
956 let Ok(env) = serde_json::from_str::<WsEnvelope>(&msg) else {
957 continue;
958 };
959 if env.channel != "trades" {
960 continue;
961 }
962 let Ok(trades) = serde_json::from_value::<Vec<WsTrade>>(env.data) else {
963 continue;
964 };
965 for trade in trades {
966 let side = if trade.side == "B" {
967 OrderSide::Buy
968 } else {
969 OrderSide::Sell
970 };
971 let price = parse_decimal(&trade.px);
972 let volume = parse_decimal(&trade.sz);
973 if let (Some(price), Some(volume)) = (price, volume) {
974 yield Ok(Fill {
975 symbol: trade.coin,
976 price,
977 volume,
978 side,
979 timestamp_ms: trade.time,
980 trade_id: trade.tid,
981 });
982 }
983 }
984 }
985 })
986 }
987}
988
989#[allow(async_fn_in_trait)]
990impl guilder_abstraction::GetAccountSnapshot for HyperliquidClient {
991 async fn get_positions(&self) -> Result<Vec<Position>, String> {
994 let user = self.require_user_address()?;
995 let resp = self
997 .info_post(
998 serde_json::json!({"type": "clearinghouseState", "user": user}),
999 2,
1000 "get_positions",
1001 )
1002 .await?;
1003 let state: ClearinghouseStateResponse = parse_response(resp).await?;
1004
1005 Ok(state
1006 .asset_positions
1007 .into_iter()
1008 .filter_map(|ap| {
1009 let p = ap.position;
1010 let size = parse_decimal(&p.szi)?;
1011 if size.is_zero() {
1012 return None;
1013 }
1014 let entry_price = p
1015 .entry_px
1016 .as_deref()
1017 .and_then(parse_decimal)
1018 .unwrap_or_default();
1019 let side = if size > Decimal::ZERO {
1020 OrderSide::Buy
1021 } else {
1022 OrderSide::Sell
1023 };
1024 Some(Position {
1025 symbol: p.coin,
1026 side,
1027 size: size.abs(),
1028 entry_price,
1029 })
1030 })
1031 .collect())
1032 }
1033
1034 async fn get_open_orders(&self) -> Result<Vec<OpenOrder>, String> {
1037 let user = self.require_user_address()?;
1038 let resp = self
1040 .info_post(serde_json::json!({"type": "openOrders", "user": user}), 20, "get_open_orders")
1041 .await?;
1042 let orders: Vec<RestOpenOrder> = parse_response(resp).await?;
1043
1044 Ok(orders
1045 .into_iter()
1046 .filter_map(|o| {
1047 let price = parse_decimal(&o.limit_px)?;
1048 let quantity = parse_decimal(&o.orig_sz)?;
1049 let remaining = parse_decimal(&o.sz)?;
1050 let filled_quantity = quantity - remaining;
1051 let side = if o.side == "B" {
1052 OrderSide::Buy
1053 } else {
1054 OrderSide::Sell
1055 };
1056 Some(OpenOrder {
1057 order_id: o.oid,
1058 symbol: o.coin,
1059 side,
1060 price,
1061 quantity,
1062 filled_quantity,
1063 })
1064 })
1065 .collect())
1066 }
1067
1068 async fn get_collateral(&self) -> Result<Decimal, String> {
1070 let user = self.require_user_address()?;
1071 let resp = self
1073 .info_post(
1074 serde_json::json!({"type": "clearinghouseState", "user": user}),
1075 2,
1076 "get_collateral",
1077 )
1078 .await?;
1079 let state: ClearinghouseStateResponse = parse_response(resp).await?;
1080 parse_decimal(&state.margin_summary.account_value)
1081 .ok_or_else(|| "invalid account value".to_string())
1082 }
1083}
1084
1085#[allow(async_fn_in_trait)]
1086impl guilder_abstraction::SubscribeUserEvents for HyperliquidClient {
1087 fn subscribe_user_fills(&self) -> BoxStream<Result<UserFill, String>> {
1088 let Some(addr) = self.user_address else {
1089 return Box::pin(stream::empty());
1090 };
1091 let addr_str = format!("{:#x}", addr);
1092 let sub = serde_json::json!({
1093 "method": "subscribe",
1094 "subscription": {"type": "userEvents", "user": addr_str.clone()}
1095 });
1096 let key = crate::ws::SubKey {
1097 channel: "userEvents".to_string(),
1098 routing_key: addr_str,
1099 };
1100 let raw_stream = self.ws_mux.subscribe(key, sub);
1101 Box::pin(raw_stream.filter_map(|text| async move {
1102 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1103 return None;
1104 };
1105 if env.channel != "userEvents" {
1106 return None;
1107 }
1108 let Ok(event) = serde_json::from_value::<WsUserEvent>(env.data) else {
1109 return None;
1110 };
1111 let items: Vec<_> = event
1112 .fills
1113 .unwrap_or_default()
1114 .into_iter()
1115 .filter_map(|fill| {
1116 let side = if fill.side == "B" {
1117 OrderSide::Buy
1118 } else {
1119 OrderSide::Sell
1120 };
1121 let price = parse_decimal(&fill.px)?;
1122 let quantity = parse_decimal(&fill.sz)?;
1123 let fee_usd = parse_decimal(&fill.fee)?;
1124 Some(UserFill {
1125 order_id: fill.oid,
1126 symbol: fill.coin,
1127 side,
1128 price,
1129 quantity,
1130 fee_usd,
1131 timestamp_ms: fill.time,
1132 })
1133 })
1134 .collect();
1135 if items.is_empty() {
1136 None
1137 } else {
1138 Some(stream::iter(items.into_iter().map(Ok)))
1139 }
1140 }).flatten())
1141 }
1142
1143 fn subscribe_order_updates(&self) -> BoxStream<Result<OrderUpdate, String>> {
1144 let Some(addr) = self.user_address else {
1145 return Box::pin(stream::empty());
1146 };
1147 let addr_str = format!("{:#x}", addr);
1148 let sub = serde_json::json!({
1149 "method": "subscribe",
1150 "subscription": {"type": "orderUpdates", "user": addr_str.clone()}
1151 });
1152 let key = crate::ws::SubKey {
1153 channel: "orderUpdates".to_string(),
1154 routing_key: addr_str,
1155 };
1156 let raw_stream = self.ws_mux.subscribe(key, sub);
1157 Box::pin(raw_stream.filter_map(|text| async move {
1158 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1159 return None;
1160 };
1161 if env.channel != "orderUpdates" {
1162 return None;
1163 }
1164 let Ok(updates) = serde_json::from_value::<Vec<WsOrderUpdate>>(env.data) else {
1165 return None;
1166 };
1167 let items: Vec<_> = updates
1168 .into_iter()
1169 .map(|upd| {
1170 let status = match upd.status.as_str() {
1171 "open" => OrderStatus::Placed,
1172 "filled" => OrderStatus::Filled,
1173 "canceled" | "cancelled" => OrderStatus::Cancelled,
1174 _ => OrderStatus::PartiallyFilled,
1175 };
1176 let side = if upd.order.side == "B" {
1177 OrderSide::Buy
1178 } else {
1179 OrderSide::Sell
1180 };
1181 OrderUpdate {
1182 order_id: upd.order.oid,
1183 symbol: upd.order.coin,
1184 status,
1185 side: Some(side),
1186 price: parse_decimal(&upd.order.limit_px),
1187 quantity: parse_decimal(&upd.order.orig_sz),
1188 remaining_quantity: parse_decimal(&upd.order.sz),
1189 timestamp_ms: upd.status_timestamp,
1190 }
1191 })
1192 .collect();
1193 if items.is_empty() {
1194 None
1195 } else {
1196 Some(stream::iter(items.into_iter().map(Ok)))
1197 }
1198 }).flatten())
1199 }
1200
1201 fn subscribe_funding_payments(&self) -> BoxStream<Result<FundingPayment, String>> {
1202 let Some(addr) = self.user_address else {
1203 return Box::pin(stream::empty());
1204 };
1205 let addr_str = format!("{:#x}", addr);
1206 let sub = serde_json::json!({
1207 "method": "subscribe",
1208 "subscription": {"type": "userEvents", "user": addr_str.clone()}
1209 });
1210 let key = crate::ws::SubKey {
1211 channel: "userEvents".to_string(),
1212 routing_key: addr_str,
1213 };
1214 let raw_stream = self.ws_mux.subscribe(key, sub);
1215 Box::pin(raw_stream.filter_map(|text| async move {
1216 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1217 return None;
1218 };
1219 if env.channel != "userEvents" {
1220 return None;
1221 }
1222 let Ok(event) = serde_json::from_value::<WsUserEvent>(env.data) else {
1223 return None;
1224 };
1225 let Some(funding) = event.funding else {
1226 return None;
1227 };
1228 let Some(amount_usd) = parse_decimal(&funding.usdc) else {
1229 return None;
1230 };
1231 let item = FundingPayment {
1232 symbol: funding.coin,
1233 amount_usd,
1234 timestamp_ms: funding.time,
1235 };
1236 Some(stream::iter(vec![Ok(item)].into_iter()))
1237 }).flatten())
1238 }
1239
1240 fn subscribe_deposits(&self) -> BoxStream<Result<Deposit, String>> {
1241 let Some(addr) = self.user_address else {
1242 return Box::pin(stream::empty());
1243 };
1244 let addr_str = format!("{:#x}", addr);
1245 let sub = serde_json::json!({
1246 "method": "subscribe",
1247 "subscription": {"type": "userNonFundingLedgerUpdates", "user": addr_str.clone()}
1248 });
1249 let key = crate::ws::SubKey {
1250 channel: "userNonFundingLedgerUpdates".to_string(),
1251 routing_key: addr_str,
1252 };
1253 let raw_stream = self.ws_mux.subscribe(key, sub);
1254 Box::pin(raw_stream.filter_map(|text| async move {
1255 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1256 return None;
1257 };
1258 if env.channel != "userNonFundingLedgerUpdates" {
1259 return None;
1260 }
1261 let Ok(ledger) = serde_json::from_value::<WsLedgerUpdates>(env.data) else {
1262 return None;
1263 };
1264 let items: Vec<_> = ledger
1265 .updates
1266 .into_iter()
1267 .filter_map(|e| {
1268 if e.delta.kind != "deposit" {
1269 return None;
1270 }
1271 let amount_usd = e.delta.usdc.as_deref().and_then(parse_decimal)?;
1272 Some(Deposit {
1273 asset: "USDC".to_string(),
1274 amount_usd,
1275 timestamp_ms: e.time,
1276 })
1277 })
1278 .collect();
1279 if items.is_empty() {
1280 None
1281 } else {
1282 Some(stream::iter(items.into_iter().map(Ok)))
1283 }
1284 }).flatten())
1285 }
1286
1287 fn subscribe_withdrawals(&self) -> BoxStream<Result<Withdrawal, String>> {
1288 let Some(addr) = self.user_address else {
1289 return Box::pin(stream::empty());
1290 };
1291 let addr_str = format!("{:#x}", addr);
1292 let sub = serde_json::json!({
1293 "method": "subscribe",
1294 "subscription": {"type": "userNonFundingLedgerUpdates", "user": addr_str.clone()}
1295 });
1296 let key = crate::ws::SubKey {
1297 channel: "userNonFundingLedgerUpdates".to_string(),
1298 routing_key: addr_str,
1299 };
1300 let raw_stream = self.ws_mux.subscribe(key, sub);
1301 Box::pin(raw_stream.filter_map(|text| async move {
1302 let Ok(env) = serde_json::from_str::<WsEnvelope>(&text) else {
1303 return None;
1304 };
1305 if env.channel != "userNonFundingLedgerUpdates" {
1306 return None;
1307 }
1308 let Ok(ledger) = serde_json::from_value::<WsLedgerUpdates>(env.data) else {
1309 return None;
1310 };
1311 let items: Vec<_> = ledger
1312 .updates
1313 .into_iter()
1314 .filter_map(|e| {
1315 if e.delta.kind != "withdraw" {
1316 return None;
1317 }
1318 let amount_usd = e.delta.usdc.as_deref().and_then(parse_decimal)?;
1319 Some(Withdrawal {
1320 asset: "USDC".to_string(),
1321 amount_usd,
1322 timestamp_ms: e.time,
1323 })
1324 })
1325 .collect();
1326 if items.is_empty() {
1327 None
1328 } else {
1329 Some(stream::iter(items.into_iter().map(Ok)))
1330 }
1331 }).flatten())
1332 }
1333}